DSL challenge. Do you guys have any elegant ideas?

Hi,
I’m writing a DSL for some parsing. And would like the following
functionality. I was wondering if there’s some metaprogramming experts
in here that can share a bit of wisdom.

Here’s the ideal functionality:

string = Farm.create do
barn do
animal “dog”
animal “cat”
end
pond do
animal “whale”
animal “shark”
end
end

The string should print:
Farm contains
[
Barn contains
[
dog
cat
]
Pond contains
[
whale
shark
]
]

It would be really really nice to have this also:
Farm.create do
animal “whale”
end

#throws "IllegalMethodError: method animal() can only be called under
barn()

I currently have a rather inelegant hack using instance_eval, which
messes up a lot of other things.
Thanks a lot for your help.
-Patrick

Patrick Li wrote:

I currently have a rather inelegant hack using instance_eval, which
messes up a lot of other things.

If you want to avoid instance_eval (and that’s a good idea for DSL
syntax in many cases, IMO), one alternative is to use yield to get
syntax like:

string = Farm.create do |farm|
farm.barn do |barn|
barn.animal “dog”
barn.animal “cat”
end
farm.pond do |pond|
pond.animal “whale”
pond.animal “shark”
end
end

A little less concise, but you avoid the scope changes that come with
instance_eval.

On Aug 9, 2008, at 5:46 PM, Patrick Li wrote:

whale
shark
]
]

It would be really really nice to have this also:
Farm.create do
animal “whale”
end

cfp: ~> cat a.rb

following is an example dsl built from my current idea of dsl best

practices, which can be found @

http://drawohara.com/post/39582749/ruby-the-best-way-to-build-ruby-dsls

f =
farm {
barn {
animal :dog
animal :cat
}

   pond {
     animal :whale
     animal :shark
   }
 }

p f

#=> #<Farm:0x250d0 @pond=#<Farm::Pond:0x24e78 @animals=[#<Whale:
0x24d74>, #Shark:0x24d38]>, @barn=#<Farm::Barn:0x24fe0
@animals=[#Dog:0x24edc, #Cat:0x24ea0]>>

BEGIN {

these classe are unimportant

 class Animal; end
 class Dog < Animal; end
 class Cat < Animal; end
 class Whale < Animal; end
 class Shark < Animal; end

this is important, note how it wraps a class so the

instance_eval is the

dsl instance_eval, note that of the wrapped object

 module Dsl
   module ClassMethods
     def dsl &block
       unless @dsl
         name = self.name.downcase.split(%r/::/).last
         @dsl = (
           Class.new do
             attr name
             const_set :Name, name
             def initialize object, &block
               ivar = "@#{ self.class.const_get(:Name) }"
               instance_variable_set ivar, object
               instance_eval &block if block
             end
           end
         )
       end
       @dsl.module_eval &block if block
       @dsl
     end
   end

   module InstanceMethods
     def dsl &block
       self.class.dsl.new(self, &block)
     end
   end

   def Dsl.included other
     other.send :extend, ClassMethods
     other.send :include, InstanceMethods
   end
 end

again this is mostly unimportant, just note how they make use of

the module

for declaring the dsl class, and how they use it in intiialize

 class Farm
   include Dsl

   attr_accessor 'barn'
   attr_accessor 'pond'

   def initialize &block
     dsl &block
   end

   class Barn
     include Dsl

     attr_accessor 'animals'

     def initialize &block
       @animals = []
       dsl &block
     end

     dsl {
       def animal name
         barn.animals << Object.const_get(name.to_s.capitalize).new
       end
     }
   end

   class Pond
     include Dsl

     attr_accessor 'animals'

     def initialize &block
       @animals = []
       dsl &block
     end

     dsl {
       def animal name
         pond.animals << Object.const_get(name.to_s.capitalize).new
       end
     }
   end

   dsl {
     def barn *a, &b
       farm.barn = Barn.new(*a, &b)
     end

     def pond *a, &b
       farm.pond = Pond.new(*a, &b)
     end
   }
 end

this is just the top level hook

 def farm(*a, &b) Farm.new(*a, &b) end

}

a @ http://codeforpeople.com/

Hi Patrick,

Here’s another solution. You didn’t specify the problems you were
having, so I can’t be certain whether this solution avoids them.

Eric

====

Ruby training and Rails training available at http://LearnRuby.com .

====

module Farm
class << self
def create(&block)
@scope_stack = []
push_frame(:farm)
class_eval(&block)

  "Farm contains\n[\n" + indent(pop_frame) + "\n]\n"
end

def barn(&block)
  push_frame(:place)
  class_eval(&block)

  append_to_frame "Barn contains\n[\n" + indent(pop_frame) +
    "\n]\n"
end

def pond(&block)
  push_frame(:place)
  class_eval(&block)

  append_to_frame "Pond contains\n[\n" + indent(pop_frame) +
    "\n]\n"
end

def animal(name)
  raise "IllegalMethodError -- animal can only be called " \
        "under a place" unless
    top_frame[0] == :place
  append_to_frame name + "\n"
end

private

def push_frame(description)
  @scope_stack.push [description, ""]
end

def pop_frame
  @scope_stack.pop[1]
end

def top_frame
  @scope_stack[-1]
end

def append_to_frame(str)
  top_frame[1] << str
end

def indent(str, level = 2)
  str.split("\n").map { |s| " "*level + s }.join("\n")
end

end
end

string = Farm.create do
barn do
animal “dog”
animal “cat”
end
pond do
animal “whale”
animal “shark”
end
end

puts string

Thanks for all the solutions.
I’ve looked through every one of them and here’s my thoughts.

Joel VanderWerf: This is the ideal functionality. It’s easily
extendable. Very nicely object-oriented. And it was the first design
that I actually used. Unfortunately, having to explicitly call the
receiver object every time is not very elegant. Which lead to me using
instance_eval to get around it.

The problem I was having with instance_eval is this:
instance_eval messes up your scope. Which is counterintuitive from the
users point of view.

i.e. the user cannot call private helper methods from within Farm.create

def barnCreator
barn do
animal “Dog”
end
end

Farm.create do
barnCreator #throws illegalmethoderror
end

Ara Howard: I think your solution solves the scope problems really
nicely. Your code library is pretty advanced though. I’m going to need
to understand it a little more fully before I can make a judgement.

Eric I: I notice that your manually controlling the “stack frame” to
restrict access to certain methods. But you’re using class_eval also,
whose scope changes will run into the same problems as instance_eval.
I’m going to need to read through your code more carefully also to
understand it.

So I guess the cleanest solution is still Joel’s. But I just need a way
to implicitly specify the receiver without changing scope and killing
all my closures.

Thanks very much for your help guys. I definately learned a lot from
your solutions.
-Patrick