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

···

--
Posted via http://www.ruby-forum.com/.

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.

···

--
       vjoel : Joel VanderWerf : path berkeley edu : 510 665 3407

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

···

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

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

#

   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/
--
we can deny everything, except that we have the possibility of being better. simply reflect on that.
h.h. the 14th dalai lama

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

···

--
Posted via http://www.ruby-forum.com/.