How Ruby’s instance_exec works

by Jason Swett,

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_execworks 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 putsinside 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 fooand 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:Objectbecause 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’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. Required fields are marked *