7
OCT2008
DSL Block Styles
There's an argument that rages in the Ruby camps: to instance_eval()
or not to instance_eval()
. Most often this argument is triggered by DSL discussions where we tend to want code like:
configurable.config do
width 100
mode :wrap
end
You can accomplish something like this by passing the block to instance_eval()
and changing self
to an object that defines the width()
and mode()
methods. Of course changing self
is always dangerous. We may have already been inside an object and planning to use methods from that namespace:
class MyObject
include Configurable # to get the config() method shown above
def initialize
config do
width calculate_width # a problem: may not work with instance_eval()
end
end
private
def calculate_width # the method we want to use
# ...
end
end
In this example, if width()
comes from a different configuration object, we're in trouble. The instance_eval()
will shift the focus away from our MyObject
instance and we will get a NoMethodError
when we try to call calculate_width()
. This may prevent us from being able to use Configurable
in our code.
A common solution is to pass the object with the width()
and mode()
methods into the block. You can then make calls on that object and keep the same self
. This could fix the above problem example:
config do |c|
c.width calculate_width
end
I imagine most of us agree that's not quite as smooth, but it tends to get viewed as a necessary evil. It's just not safe to always use instance_eval()
. I think there are some issues with that line of thinking though:
- Sometimes
instance_eval()
is OK and we would prefer to have the prettier syntax when it is - Library authors are making this blanket decision for all the use cases
- We have a super dynamic language here so we should be able to have it both ways
It turns out that we can accommodate both schools of thought rather easily. You can ask Ruby to bundle up any block into a Proc
object and Proc
objects have an arity()
method that will tell you how many arguments they expect. We can use that to determine when to switch strategies:
class DSL
def initialize(&dsl_code) # creates the Proc
if dsl_code.arity == 1 # the arity() check
dsl_code[self] # argument expected, pass the object
else
instance_eval(&dsl_code) # no argument, use instance_eval()
end
end
def do_something
puts "Doing something..."
end
end
DSL.new { |d| d.do_something }
# >> Doing something...
DSL.new { do_something }
# >> Doing something...
Users of this code can now decide how they want it to work based on their needs as the examples show.
With a language like Ruby these limitations just become opportunities for showing off how dynamic our code can be. Don't be so quick to give in to necessary evils.
Comments (7)
-
Trans October 7th, 2008 Reply Link
The work around.
def initialize this = self config do width this.calculate_width end end
-
Yeah, but gross, right? :)
-
-
You might check out _why's mixico: http://hackety.org/2008/10/06/mixingOurWayOutOfInstanceEval.html
I haven't had a chance to play with it yet, but it seems to be another approach to the same problem.
-
Jim Weirich also posted a pure Ruby MethodDirector implementation to Ruby Core for mixing the methods of multiple objects into one namespace.
-
it's a nice idea, thanks :)
-
What do you think of how the Docile gem approaches this?
https://github.com/ms-ati/docile-
Yeah, that seems like a fine approach.
-