DSL challenge. Do you guys have any elegant ideas?

P

Patrick Li

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
 
J

Joel VanderWerf

Patrick said:
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.
 
A

ara.t.howard

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



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/
 
E

Eric I.

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
 
P

Patrick Li

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
 

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. After that, you can post your question and our members will help you out.

Ask a Question

Similar Threads

ANN main-4.4.0 0
[ANN] main-4.0.0 (for avdi) 0
[ANN] main-3.0.1 0
[ANN] main-2.8.3 2
[ANN] main-2.1.0 6
[ANN] main-2.6.0 0
[ANN] main-2.5.0 4
[ANN] main-2.2.0 2

Members online

Forum statistics

Threads
473,764
Messages
2,569,566
Members
45,041
Latest member
RomeoFarnh

Latest Threads

Top