Wednesday, July 12, 2006

Ruby Block Scope

While doing DSL work you constantly evaluate blocks (or code as strings) in various contexts. In fact, one key to using a DSL is the ability to evaluate in various contexts. Take this code for example:
class SqlGenerator
class << self
def evaluate(&script)
self.new.instance_eval(&script)
end
end

def multiply(arg)
"select #{arg}"
end

def two(arg=nil)
"2#{arg}"
end

def times(arg)
" * #{arg}"
end
end
The above code allows you to generate a SQL statement by calling the SqlGenerator.evaluate method with a block:
SqlGenerator.evaluate { multiply two times two }
=> "select 2 * 2"
However, you could also execute the same code in the context of a calculator class to receive a result:
class Calculator
class << self
def evaluate(&script)
self.new.instance_eval(&script)
end
end

def multiply(arg)
eval arg
end

def two(arg=nil)
"2#{arg}"
end

def times(arg)
" * #{arg}"
end
end
Which executes as:
Calculator.evaluate { multiply two times two }
=> 4
The (obvious) trick here is to use instance_eval to specify the scope in which the block executes. The instance_eval method evaluates either a string or block within the context of the receiver. The receivers in my examples were a new instance of SqlGenerator and a new instance of Calculator. When instance_eval is called on it's own, self is the receiver. Also, be sure to note that I call self.new.instance_eval. If I don't call the new method of self, the block will be evaluated by the class and not an instance of the class.

However, instance_eval is only one of the ways a block can be executed. Blocks can also be executed by calling the call method:
def go(&block)
block.call
end
go { 'called' }
=> "called"
In this case, the scope at creation time is used for evaluation.
class Foo
def bar
"bar"
end

def do_something(&block)
block.call
end
end
Foo.new.do_something { bar }
NameError: undefined local variable or method `bar' for main:Object
from (irb):18
from (irb):15:in `do_something'
from (irb):18
from :0
In the above example, even though bar is defined in Foo the block was created in the context of Object (in irb) and bar is not defined in object. If you change the code to be:
def bar
"this bar is called, but it's not the bar in Foo"
end
class Foo
def bar
"bar"
end

def do_something(&block)
block.call
end
end
Foo.new.do_something { bar }
=> "this bar is called, but it's not the bar defined in Foo"
Well, you see the results.

Another way to execute a block is by using the yield statement. The yield statement also executes using the context in which the block was defined.
def bar
"this bar is called, but it's not the bar in Foo"
end
class Foo
def bar
"bar"
end

def do_something
yield
end
end
Foo.new.do_something { bar }
=> "this bar is called, but it's not the bar defined in Foo"
Whether to use yield, block.call, or instance_eval all depends on the context in which you need the code evaluated.
Post a Comment