In this post we’ll take a look at Ruby’s instance_exec
, a method which can can change the execution context of a block and help make DSL syntax less noisy.
Passing arguments to blocks
When calling a Ruby block using block.call
, you can pass an argument (e.g. block.call("hello")
and the argument will be fed to the block.
Here’s an example of passing an argument when calling a block.
def word_fiddler(&block)
block.call("hello")
end
word_fiddler do |word|
puts word.upcase
end
In this case, the string "hello"
gets passed for word
, and word.upcase
outputs HELLO
.
We can also do something different and perhaps rather strange-seeming. We can use a method called instance_exec
to execute our block in the context of whatever argument we send it.
Executing a block in a different context
Note how in the following example word.upcase
has changed to just upcase
.
def word_fiddler(&block)
"hello".instance_exec(&block)
end
word_fiddler do
puts upcase
end
The behavior is the exact same. The output is identical. The only difference is how the behavior is expressed in the code.
How this works
Every command in Ruby operates in a context. Every context is an object. The default context is an object called main
, which you can demonstrate by opening a Ruby console and typing self
.
We can also demonstrate this for our earlier word_fiddler
snippet.
def word_fiddler(&block)
block.call("hello")
end
word_fiddler do |word|
puts self # shows the current context
puts word.upcase
end
If you run the above snippet, you’ll see the following output:
main
HELLO
The instance_exec
method works because in changes the context of the block it invokes. Here’s our instance_exec
snippet with a puts self
line added.
def word_fiddler(&block)
"hello".instance_exec(&block)
end
word_fiddler do
puts self
puts upcase
end
Instead of main
, we now get hello
.
hello
HELLO
Why instance_exec is useful
instance_exec
can help make Ruby DSLs less verbose.
Consider the following Factory Bot snippet:
FactoryBot.define do
factory :user do
first_name { 'John' }
last_name { 'Smith' }
end
end
The code above consists of two blocks, one nested inside the other. There are three methods called in the snippet, or more precisely, there are three messages being sent: factory
, first_name
and last_name
.
Who is the recipient of these messages? In other words, in what contexts are these two blocks being called?
It’s not the default context, main
. The outer block is operating in the context of an instance of a class called FactoryBot::Syntax::Default::DSL
, which is defined by the Factory Bot gem. This means that the factory
message is getting sent to an instance of FactoryBot::Syntax::Default::DSL
.
The inner block is operating in the context of a different object, an instance of FactoryBot::Declaration::Implicit
. The first_name
and last_name
messages are getting sent to this class.
You can perhaps imagine what the Factory Bot syntax would have to look like if it were not possible to change blocks’ contexts using instance_exec
. The syntax would be pretty verbose and noisy.
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!
Good article, but let me add a suggestion:
I think that for a better understanding of how `instace_exec` works, and to clearly see the difference with the former example, you could modify the method `word_fiddler` as this:
word_fiddler do
puts self + ‘ Jason’
end
The result will be “hello Jason”, so the “self” within the `word_fidler` method is the string “hello”