Saturday, July 22, 2006

Ruby eval with a binding

I recently discussed how you can evaluate blocks in different scopes by using instance_eval, yield, and block.call. In this entry I'll show how you can evaluate a string of ruby code in the scope of a block.

Lets start with a simple example:
class Foo
def create_block
Proc.new {}
end
end
In 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.
proc = Foo.new.create_block

eval "self.class", proc.binding #=> Foo
When 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:
Select[:column1, :column2].from[:table1, :table2].where do
equal table1.id, table2.table1_id
end
When the above code is evaluated the [] instance method (following 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 do
exists(Select[:column2].from[:table2].where do
equal table1.column1, table2.column2
end)
end
Unfortunately, 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).

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.binding
Which 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 .. end
This 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.
Post a Comment