What instance_exec is and why it exists
instance_exec
is a method that executes a block in the context of whatever block you give it.
Why would we want to do this? As we’ll see, the usage of instance_exec
can make DSL code less noisy and verbose. We can see based on a couple examples of DSL code that benefits from instance_exec
. The first example is the code for a Factory Bot factory definition.
FactoryBot.define do
factory :user do
first_name { "John" }
last_name { "Smith" }
end
end
In this snippet, the factory
method seems to come from nowhere. We’ll see shortly how this mystery can be explained.
This RSpec snippet has similar mysteries. Where does the describe
method on the second line come from? How about the it
method on the third line?
RSpec.describe "Hello world", type: :system do
describe "index page" do
it "shows the right content" do
visit hello_world_index_path
expect(page).to have_content("Hello, world!")
end
end
end
In this chapter we’re going to demonstrate how libraries like Factory Bot and RSpec use instance_exec
in order to achieve the effect shown above.
A custom use of instance_exec
Perhaps the easiest way to illustrate how instance_exec
works is for us to go through the process of writing our own custom method that uses instance_exec
.
Here’s a piece of code that’s loosely analogous to the Factory Bot and RSpec examples above. What this code has in common with the above examples is that there’s a method of mysterious origin (content
) being called inside a block.
inside_tag("p") do
content "Hello"
content "World"
end
We’re going to write a definition for the inside_tag
method in such a way that, when we call it in the above manner, the following output is produced.
<p>
Hello
World
</p>
First pass at the inside_tag method
Here’s a crude version of the inside_tag
method which just uses puts
inside the block instead of the content
method calls we’re ultimately after.
def inside_tag(name, &block)
puts "<#{name}>"
block.call
puts "</#{name}>"
end
inside_tag("p") do
puts " Hello"
puts " World"
end
This code produces the desired output but we’re not using a content
method yet. It’s also a little bit inelegant that we have to achieve the indentation by manually prefixing each line with a space. We’ll address that shortcoming in the next pass.
Second pass at the inside_tag method
Here’s a version of the method that’s slightly closer to what we’re shooting for. In this version, we define a class called Tag
, an instance of which is passed to the block so that the body of the block can call the Tag
object’s content
method. Doing it this way also allows us to add an indent to each piece of content automatically.
class Tag
def content(value)
puts " #{value}"
end
end
def inside_tag(name, &block)
puts "<#{name}>"
block.call(Tag.new)
puts "</#{name}>"
end
inside_tag("p") do |tag|
tag.content "Hello"
tag.content "World"
end
Third pass at inside_tag using instance_exec
Lastly, we can use instance_exec
to make the block execute in the context of a Tag
object. We now no longer have to do tag.content
because the block is already executing in the context of a Tag
.
class Tag
def content(value)
puts " #{value}"
end
end
def inside_tag(name, &block)
puts "<#{name}>"
Tag.new.instance_exec(&block)
puts "</#{name}>"
end
inside_tag("p") do
content "Hello"
content "World"
end
Now that we’ve seen that this works, let’s take a closer look at why it works.
Why this works
The main object
Every method that’s called in Ruby is called in the context of some object.
Even when we don’t seem to be in the context of any object, we are. If you open an irb
console and type self
, you’ll see that the return value is main
.
> self
=> main
There’s an object called main
which receives any message that we send from the console.
If you type foo
and hit enter, you’ll get an error that says:
NameError (undefined local variable or method `foo' for main:Object)
This is proof that when we send a message to no object in particular, the recipient of that message is main
. (It says main:Object
because main
is an instance of the native Ruby class called Object
.)
Changing the context
Put the following code into a Ruby file and then run it.
class Tag
def execute1(&block)
block.call
end
def execute2(&block)
instance_exec(&block)
end
end
puts "Current context is #{self}"
Tag.new.execute1 { puts "Current context is #{self}" }
Tag.new.execute2 { puts "Current context is #{self}" }
The output will look something like this:
Current context is main
Current context is main
Current context is #<Tag:0x0000000155045078>
The first and second lines output main
because, as was said earlier, the default recipient for any message that’s not sent to a particular object is main
.
The third line outputs Tag
because the Tag#execute2
method invokes the block it’s given using instance_exec
.
Takeaways
instance_exec
is a method that executes a block in the context of a certain object.instance_exec
can help make DSL syntax less noisy and verbose.- Methods like Factory Bot’s
factory
and RSpec’sit
anddescribe
are possible because ofinstance_exec
.
Excellent article.
I like your simple explanations!
Thank you.
Thanks!
Great article.
Is there a way for having “both of best world”, which means, the block being passed also having access to it’s original lexical scope?
What I mean by this is that the block being passed as an argument by `inside_tag` will not be able to call methods that are in the same scope as the `inside_tag` method. Is there a way around this?
Thanks!
Unfortunately I’m not sure that I understand your question. If you can share a code example of the scenario you’re describing (maybe with a link to a GitHub gist) that would help.
Sure Jason, here you go: https://gist.github.com/feliperaul/372bf2732509f3eea1cff9c7a06d7718
Take a look that the `first_name` method was in scope inside the block, but since the `greet_with` method uses instance_exec it won’t be when the block is run.
You could do it like this, although TBH it feels like a hack and I can’t think of a scenario where you would want to do such a thing: https://gist.github.com/jasonswett/9808c02f7add3e0c3837adba53ce44f4
TIL, thanks for the article!
Very good article 🙂
Thanks!
Very well explained 🙂 specially breaking it point wise and then spicing things up.
Thanks!