How does instance_eval work and why does DHH hate it?

Ok, so the idea here is instead of something like this

form_for @obj do |f|
  f.text_field :field
end

you get something like this

form_for @obj do 
  text_field :field
end

the first way is pretty straight forward, you end up with a pattern that looks like this

def form_for
  b = FormBuilder.new
  yield b
  b.fields.each |f|
    # do stuff
  end
end

you yield out a builder object that the consumer calls methods on, and afterwards you call methods on the builder object to actually build the form (or whatever)

the second one is a bit more magical

def form_for &block
  b = FormBuilder.new
  b.instance_eval &block
  b.fields.each |f|
    #do stuff
  end
end

in this one, instead of yielding the builder to the block, we take the block and evaluate it in the context of the builder

The second one increases complexity because you are sort of playing games with scope, you need to understand that, and the consumer needs to understand that, and whoever wrote your builder needs to understand that. If everyone is on the same page, I don't know that it is nessicarily a bad thing, but i do question the benefits vs the costs, i mean, how hard is it to just tack on an f. in front of your methods?


The thing that instance_eval does is that it runs the block in the context of a different instance. In other words, it changes the meaning of self which means it changes the meaning of instance methods and instance variables.

This creates a cognitive disconnect: the context in which the block runs is not the context in which it appears on the screen.

Let me demonstrate that with a slight variation of @Matt Briggs's example. Let's say we're building an email instead of a form:

def mail
  builder = MailBuilder.new
  yield builder
  # executed after the block 
  # do stuff with builder 
end

mail do |f|
  f.subject @subject
  f.name    name
end

In this case, @subject is an instance variable of your object and name is a method of your class. You can use nice object-oriented decomposition and store your subject in a variable.

def mail &block
  builder = MailBuilder.new
  builder.instance_eval &block
  # do stuff with builder 
end

mail do 
  subject @subject
  name    name # Huh?!?
end

In this case, @subject is an instance variable of the mail builder object! It might not even exist! (Or even worse, it might exist and contain some completely stupid value.) There is no way for you to get access to your object's instance variables. And how do you even call the name method of your object? Everytime you try to call it, you get the mail builder's method.

Basically, instance_eval makes it hard to use your own code inside the DSL code. So, it should really only be used in cases where there is very little chance that this might be needed.