Ruby DSLs: instance_eval with delegation

I saw the warning issued on Ola's blog: don't overuse instance_eval. JEG II blogged about a compromise, and why had an idea. But I like variation of Jim Weirich's MethodDirector.

how instance_eval works

If you're not familiar with instance_eval , it evaluates a block of code with self set to the object that's receiving the instance_eval call. Here's an example.

add_two = Proc . new { self + 2 } puts 1 . instance_eval (& add_two ) puts 2 . instance_eval (& add_two )

Here's a second example using an implicit method receiver.

call_reverse = Proc . new { reverse } p " abc ". instance_eval (& call_reverse ) p [" x ", " y ", " z "]. instance_eval (& call_reverse )

the problem with instance_eval

Ola described it really well, so I'm just going to quote him and then show an example.

So what's the problem with it? Well, the problem is that blocks are generally closures. And you expect them to actually be full closures. And it's not obvious from the point where you write the block that that block might not be a full closure. That's what happens when you use instance_eval: you reset the self of that block into something else - this means that the block is still a closure over all local variables outside the block, but NOT for method calls. I don't even know if constant lookup is changed or not. Using instance_eval changes the rules for the language in a way that is not obvious when reading a block. You need to think an extra step to figure out exactly why a method call that you can lexically see around the block can actually not be called from inside of the block.

Here's an example. Let's take a look at a quasi migration using create_table and _not_ using instance_eval .

class MyMigration < MigrationExample def self.up create_table " people " do | t | t . string " first_name ", " last_name " end end end

To implement this, we could need t to reference a TableDefinition class that would be used to build up columns. But we could get rid of t if we were to use instance_eval . Because instance_eval could change self to reference the TableDefinition , we could use the implicit method receiver and change the migration to look something like this.

class MyMigration < MigrationExample def self.up create_table " people " do string " first_name ", " last_name " end end end

But let's take a look at the consequences of this approach. Here's a little fake migration class that just prints output.

class MigrationExample def self.create_table ( table_name , & block ) table = TableDefinition . new table_name table . evaluate & block table . create end end class TableDefinition def initialize ( table_name , & block ) @table_name = table_name @columns = [] end def evaluate (& block ) instance_eval & block end def string (* columns ) @columns << columns end def create puts " creating the ' #{@table_name} ' table with columns: #{@columns.join(", ")} " end end

So here we're using instance_eval to evaluate the block passed to create_table . And if we run the migration, we'll see that the table gets created.

MyMigration.up #=> creating the 'people' table with columns: first_name, last_name

But what happens if we want to use a little helper method in our migration? Let's try it by doing something completely trivial and extracting a name method.

class MyMigration & lt ; MigrationExample def self.up create_table " people " do string name (" first "), name (" last ") end end def self.name ( which ) " #{which} _name " end end

And now if we run the migration...

MyMigration.up # NoMethodError: undefined method 'name' for #<TableDefinition:0x89e18 @columns=[], @table_name="people">

Ouch. Because self is set to the TableDefinition class while instance_evaling the block, our method call to name was sent to the table definition object instead of our migration class.

Here's one approach to solve this problem.

instance_eval + delegation

So the problem is that instance_eval hijacks self , making our method call to name fail. But what if we can get the name call sent back to the object that it would have gone to if we didn't change self ?

We can accomplish this by capturing what self is before we change it with instance_eval . Then if our class doesn't respond to a method, we'll assume it was meant for the original receiver.

class TableDefinition def evaluate (& block ) @self_before_instance_eval = eval " self ", block . binding instance_eval & block end def method_missing ( method , * args , & block ) @self_before_instance_eval . send method , * args , & block end end

Looking at the create_table block again...

create_table " people " do string name (" first "), name (" last ") end

The call to string will be called on TableDefinition . But the name method call will hit method missing and be delegated back to the migration. Here's the output if we run the migration again.

MyMigration.up # creating the 'people' table with columns: first_name, last_name

We've removed the need for the block local variable t , but also preserved the capability to use local helper methods.

summary