There's an important difference between instance_eval
and instance_exec
. And there's a great lesson about how to use them well in FactoryGirl
But first, before you go rushing off to build your fantastic DSL, let's look at what instance_eval
is and does.
The simplest of examples can be taken straight from the Ruby docs:
class KlassWithSecret
def initialize
@secret = 99
end
end
k = KlassWithSecret.new
k.instance_eval { @secret } #=> 99
The current value for self
inside the provided block will be the object on which you call instance_eval
. So in this case the k
object is the current context for the block; @secret
is a variable stored inside k
and instance_eval
opens up access to that object and all of it's internal variables.
The interface that FactoryGirl provides is simple and straightforward. Here's an example from it's "Getting Started" documentation:
FactoryGirl.define do
factory :user do
first_name "Kristoff"
last_name "Bjorgman"
admin false
end
end
Here, FactoryGirl uses instance_eval
to execute the blocks of code passed to factory
.
Let's take a look at some representative code from how FactoryGirl makes this work:
def factory(name, &block)
factory = Factory.new(name)
factory.instance_eval(&block) if block_given?
# ... more code
end
That's not actually the code from FactoryGirl, but it represents roughly what happens. When the method factory
is called a new Factory
is created and then the block is executed in the context of that object. In other words where you see first_name
it's as if you had that factory instance before it and instead had factory.first_name
. By using instance_eval
, the users of FactoryGirl don't need to specify the factory object, it's implicitly applied to it.
_Ok, that's all well and good, but what about instance_exec
?_
I'm glad you asked.
The instance_eval
method can only evaluate a block (or a string) but that's it. Need to pass arguments into the block? You'll be frozen in your tracks.
But instance_exec
on the other hand, will evaluate a provide block and allow you to pass arguments to it. Let's take a look...
FactoryGirl allows you to handle callbacks to perform some action, for example, after the object is created.
FactoryGirl.define do
factory :user do
first_name "Kristoff"
last_name "Bjorgman"
admin false
after(:create) do |user, evaluator|
create_list(:post, evaluator.posts_count, user: user)
end
end
end
In this sample, the after(:create)
is run after the object is created, but the block accepts two arguments: user
and evaluator
. The user
argument is the user that was created. The evaluator
is an object which stores all the values created by the factory.
Let's take a look at how this is implemented:
def run(instance, evaluator)
case block.arity
when 1, -1 then syntax_runner.instance_exec(instance, &block)
when 2 then syntax_runner.instance_exec(instance, evaluator, &block)
else syntax_runner.instance_exec(&block)
end
end
FactoryGirl will create a callback object named by the argument given to the after
method. The callback is created with a name, :create
in this case, and with a block of code.
The block that we used in our example had two arguments.
The run
method decides how to execute the code from the block.
The callback object stores the provided block and Ruby allows us to check the arity of the block, or in other words, it allows us to check the number of arguments.
When looking at a case
statement, it's a good idea to check the else
clause first. This gives you an idea of what will happen if there's no match for whatever code exists in the when
parts.
There we see syntax_runner.instance_exec(&block)
and this could easily be changed to use instance_eval
instead. Ruby will evaluate, or execute, the block in the context of the syntax_runner
object.
If the block's arity is greater than zero, FactoryGirl needs to provide the objects to the block so that our code works the way we expect.
The second part of the case checks if the block arity is equal to 2
.
when 2 then syntax_runner.instance_exec(instance, evaluator, &block)
If it is, the syntax_runner
receives the instance
(or in our case user
) and the evaluator
.
If, however, the arity is 1
or -1
then the block will only receive the instance
object.
So what is that -1
value? Let's look at the ways we could create a callback:
# Two arguments and arity of 2
after(:create) do |user, evaluator|
create_list(:post, evaluator.posts_count, user: user)
end
# One argument and arity of 1
after(:create) do |user|
create_group(:people, user: user)
end
# Zero arguments and arity of 0
after(:create) do
puts "Yay!"
end
# Any arguments and arity of -1
after(:create) do |*args|
puts "The user is #{args.first}"
end
Ruby doesn't know how many args
you'll give it with *args
so it throws up it's hands and tells you that it's some strange number: -1
.
This is the power of understanding how and when to use instance_exec
; users of the DSL will expect it to make sense, and it will.
But wait! There's more!
What if you want to specify the same value for multiple attributes?
FactoryGirl.define do
factory :user do
first_name "Kristoff"
last_name "Bjorgman"
password "12345"
password_confirmation "12345"
end
end
In the above example, both the password
and password_confirmation
are set to the same value. This could be bad.
What if you change the password for one, but forget to change the other? If they are inherently tied in their implementation, then that could lead to some unexpected behavior when they are not the same.
I would, and probably you would too, prefer to tell FactoryGirl to just use the value I'd already configured.
Fortunately FactoryGirl allows us to use a great trick in Ruby using the to_proc
method. Here's what it looks like in use:
FactoryGirl.define do
factory :user do
first_name "Kristoff"
last_name "Bjorgman"
password "12345"
password_confirmation &:password
end
end
The important part is the &:password
value provided to password_confirmation
. Ruby will see the &
character and treat the following as a block by calling to_proc
on it. To implement this feature, FactoryGirl defines to_proc
on attributes and there will use instance_exec
to provide the symbol password
to the block:
def to_proc
block = @block
-> {
value = case block.arity
when 1, -1 then instance_exec(self, &block)
else instance_exec(&block)
end
raise SequenceAbuseError if FactoryGirl::Sequence === value
value
}
end
What about lambdas and procs?
Some commenters in Reddit raised an important question about how these methods behave when given lambdas and procs.
If you provide a lambda which accepts no arguments as the block, instance_eval
will raise an error:
object = Object.new
argless = ->{ puts "foo" }
object.instance_eval(&argless) #=> ArgumentError: wrong number of arguments (1 for 0)
This error occurs because Ruby will yield the current object to the provided block as self
. So you can fix it by providing a lambda which accepts an argument:
args = ->(obj){ puts "foo" }
object.instance_eval(&args) #=> "foo"
This changes a bit if you use instance_exec
:
object.instance_exec(&argless) #=> "foo"
object.instance_exec(&args) #=> ArgumentError: wrong number of arguments (0 for 1)
object.instance_exec("some argument", &args) #=> "foo"
Because a proc
is less restrictive with argument requirements, it will allow either approach to work without error:
p_argless = proc{ puts "foo" }
object.instance_eval(&p_argless) #=> "foo"
p_args = proc{|obj| puts "foo" }
object.instance_eval(&p_args) #=> "foo"
object.instance_exec(&p_args) #=> "foo"
object.instance_exec(&p_argless) #=> "foo"
Now you know, instance_exec
and instance_eval
are similar in the way they behave, but you'll reach for instance_exec
if you need to pass variables around.
##Announcing Ruby Metaprogramming Masterclass
I'm offering a new online class where I'll be teaching you how to master metaprogramming in Ruby on April 30th (the day after my birthday!)
I'm keeping the spaces limited to 25 so attendees will be able to talk and ask questions but already over a quarter of the seats are gone. So grab a seat now, before they're all gone.