Lets start with a simple example:
class FooIn the example the Foo class has a method, create_block, that simply creates and returns a block. Despite the fact that this doesn't look very interesting, it actually provides you with the ability to evaluate any string of ruby code in the scope of the block. This gives you the power to evaluate ruby code in the context of an object without directly having reference to it.
def create_block
Proc.new {}
end
end
proc = Foo.new.create_blockWhen would you ever actually use such a thing? I recently used it on my current project while developing a DSL to represent SQL. Our current syntax looks like this:
eval "self.class", proc.binding #=> Foo
Select[:column1, :column2].from[:table1, :table2].where doWhen the above code is evaluated the [] instance method (following
equal table1.id, table2.table1_id
end
from
) saves each table name in an array. Then, when the where
method is executed the method_missing
method is called twice, once with :table1 and then with :table2. When method_missing is called we check that table name array includes the symbol argument to verify it is a valid table name. If the table name is valid we return an object that knows how to react to column names. However, if the table name is invalid we call super
and raise NameError.All of this works perfectly, until you start using sub-queries. For example, the following code will not work with the current implementation.
Delete.from[:table1].where doUnfortunately, this needed to work, and using eval and specifying a binding was how we made it work. The trick is getting the table names array from the outer block into the inner block without explicitly passing it in somehow (which would have made the DSL ugly).
exists(Select[:column2].from[:table2].where do
equal table1.column1, table2.column2
end)
end
To solve this problem, within the where method we added:
tables.concat(eval("respond_to?(:tables) ? tables : []", block.binding))Let's break this statement apart and see what it's doing. The first thing it's going to do is
eval "respond_to?(:tables) ? tables : []", block.bindingWhich simply means "eval that statement within the scope of the block". In this case the scope of that block is:
Delete.from[:table1].where do .. endThis scope is a Delete instance, which does have a tables array (tables #=> [:table1]). Therefore, the statement will evaluate and return the tables array, and the rest of the statement could be pictured as this:
tables.concat([:table1])This is simply going to concatenate all the table names into the tables array available in the inner block. After this one line change, the statement now produces expected results.
delete from table1 where exists (select column2 from table2 where table1.column1 = table2.column2)For more info see eval in the core documentation.
Jay, you're awesome! I read your previous blog post just the other day to help me out with my own project and came across this exact issue myself! I guess Jays think alike. :)
ReplyDeleteKeep up the great blogging!
Jay Phillips
http://jicksta.com
Hi Jay,
ReplyDeleteWas wondering if you've ever needed to call a method that accepts a block in the scope of a given proc passing in an existing proc reference i.e.
class A
def a_method
'bar'
end
def capture(&block)
block.call
end
def the_proc
Proc.new {}
end
end
a = A.new
b = Proc.new { puts 'foo ' + a_method }
p = a.the_proc
eval("capture(&#{want_to_give_proc_b_as_block?})", p.binding)
This is really bugging me, I can't find a nice solution.
Ignore that comment, I was missing the obvious:
ReplyDeleteExample usage within a module that knows nothing about the Rails template object but wants to define a block helper.
module CaptureExample
def self.block_helper(title, &block)
template = eval('self', block.binding)
template.concat(template.content_tag('h2', title) + template.capture(&block))
end
end
# In the template
<% CaptureExample.block_helper 'Hello' do %>
This is using a helper that know <%= content_tag 'strong', 'nothing' %> about the template.
<% end %>
Thanks again for a very illuminating post! This was very useful for me where I wanted the user to be able to pass arbitrary constraints into a ruby program. User supplies a string, e.g. "price < 10 && inventory > 0", and this gets evaluated in the context of a series of ruby objects until it returns true. Sweet!
ReplyDelete