DSL challenge. Do you guys have any elegant ideas?

Discussion in 'Ruby' started by Patrick Li, Aug 10, 2008.

  1. Patrick Li

    Patrick Li Guest

    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, Aug 10, 2008
    #1
    1. Advertisements

  2. 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.
     
    Joel VanderWerf, Aug 10, 2008
    #2
    1. Advertisements

  3. Patrick Li

    ara.t.howard Guest



    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/
     
    ara.t.howard, Aug 10, 2008
    #3
  4. Patrick Li

    Eric I. Guest

    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
     
    Eric I., Aug 10, 2008
    #4
  5. Patrick Li

    Patrick Li Guest

    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
     
    Patrick Li, Aug 10, 2008
    #5
    1. Advertisements

Ask a Question

Want to reply to this thread or ask your own question?

You'll need to choose a username for the site, which only take a couple of moments (here). After that, you can post your question and our members will help you out.