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.

4 comments:

  1. 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. :)

    Keep up the great blogging!

    Jay Phillips
    http://jicksta.com

    ReplyDelete
  2. Anonymous3:19 PM

    Hi Jay,

    Was 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.

    ReplyDelete
  3. Anonymous5:16 PM

    Ignore that comment, I was missing the obvious:

    Example 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 %>

    ReplyDelete
  4. 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

Note: Only a member of this blog may post a comment.