How Ruby’s instance_exec works

by Jason Swett,

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’s it and describe are possible because of instance_exec.

11 thoughts on “How Ruby’s instance_exec works

  1. fraul

    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?

    Reply
    1. Jason Swett Post author

      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.

      Reply

Leave a Reply

Your email address will not be published.