Understanding instance exec in ruby

Neeraj Singh

Neeraj Singh

March 12, 2013

In ruby procs have lexical scoping. What does that even mean. Let's start with a simple example.

1square = lambda { x * x }
2x = 20
3puts square.call()
4# => undefined local variable or method `x' for main:Object (NameError)

So even though variable x is present, the proc could not find it because when the code was read then x was missing .

Let's fix the code.

1x = 2
2square = lambda { x * x }
3x = 20
4puts square.call()
5# => 400

In the above case we got the answer. But the answer is 400 instead of 4 . That is because the proc binding refers to the variable x. The binding does not hold the value of the variable, it just holds the list of variables available. In this case the value of x happens to be 20 when the code was executed and the result is 400 .

x does not have to a variable. It could be a method. Check this out.

1square = lambda { x * x }
2def x
3  20
4end
5puts square.call()
6# => 400

In the above case x is a method definition. Notice that binding is smart enough to figure out that since no x variable is present let's try and see if there is a method by name x .

Another example of lexical binding in procs

1def square(p)
2   x = 2
3   puts p.call
4end
5x = 20
6square(lambda { x * x })
7#=> 400

In the above case the value of x is set as 20 at the code compile time. Don't get fooled by x being 2 inside the method call. Inside the method call a new scope starts and the x inside the method is not the same x as outside .

Issues because of lexical scoping

Here is a simple case.

1class Person
2  code = proc { puts self }
3
4  define_method :name do
5    code.call()
6  end
7end
8
9class Developer < Person
10end
11
12Person.new.name # => Person
13Developer.new.name # => Person

In the above case when Developer.new.name is executed then output is Person. And that can cause problem. For example in Ruby on Rails at a number of places self is used to determine if the model that is being acted upon is STI or not. If the model is STI then for Developer the query will have an extra where clause like AND "people"."type" IN ('Developer') . So we need to find a solution so that self reports correctly for both Person and 'Developer` .

instance_eval can change self

instance_eval can be used to change self. Here is refactored code using instance_eval .

1class Person
2  code = proc { puts self }
3
4  define_method :name do
5    self.class.instance_eval &code
6  end
7end
8
9class Developer < Person
10end
11
12Person.new.name #=> Person
13Developer.new.name #=> Developer

Above code produces right result. However instance_eval has one limitation. It does not accept arguments. Let's change the proc to accept some arguments to test this theory out.

1class Person
2  code = proc { |greetings| puts greetings; puts self }
3
4  define_method :name do
5    self.class.instance_eval 'Good morning', &code
6  end
7end
8
9class Developer < Person
10end
11
12Person.new.name
13Developer.new.name
14
15#=> wrong number of arguments (1 for 0) (ArgumentError)

In the above case we get an error. That's because instance_eval does not accept arguments.

This is where instance_exec comes to rescue. It allows us to change self and it can also accept arguments.

instance_exec to rescue

Here is code refactored to use instance_exec .

1class Person
2  code = proc { |greetings| puts greetings; puts self }
3
4  define_method :name do
5    self.class.instance_exec 'Good morning', &code
6  end
7end
8
9class Developer < Person
10end
11
12Person.new.name #=> Good morning Person
13Developer.new.name #=> Good morning Developer

As you can see in the above code instance_exec reports correct self and the proc can also accept arguments .

Conclusion

I hope this article helps you understand why instance_exec is useful.

I scanned RubyOnRails source code and found around 26 usages of instance_exec . Look at the usage of instance_exec usage there to gain more understanding on this topic.

If this blog was helpful, check out our full blog archive.

Stay up to date with our blogs.

Subscribe to receive email notifications for new blog posts.