instance_exec , changing self and params

 
 

Here is updated article on the same topic .

Following code will print 99 as the output.

class Klass
  def initialize
    @secret = 99
  end
end
puts Klass.new.instance_eval { @secret }

Nothing great there. However try passing a paramter to instance_eval .

puts Klass.new.instance_eval(self) { @secret }

You will get following error.

wrong number of arguments (1 for 0)

So instance_eval does not allow you to pass parameters to a block.

How to get around to the restriction that instance_eval does not accept parameters

instance_exec was added to ruby 1.9 and it allows you to pass parameters to a proc. This feature has been backported to ruby 1.8.7 so we don't really need ruby 1.9 to test this feature. Try this.

class Klass
  def initialize
    @secret = 99
  end
end
puts Klass.new.instance_exec('secret') { |t| eval"@#{t}" }

Above code works. So now we can pass parameters to block. Good.

Changing value of self

Another feature of instance_exec is that it changes the value of self. To illustrate that I need to give a longer example.

module Kernel
  def singleton_class
    class << self
      self
    end
  end
end

class Human
  proc = lambda { puts 'proc says my class is ' + self.name.to_s }

  singleton_class.instance_eval do
    define_method(:lab)  do
      proc.call
    end
  end
end

class Developer < Human
end

Human.lab # class is Human
Developer.lab # class is Human ; oops

Notice that in that above case Developer.lab says "Human". And that is the right answer from ruby perspective. However that is not what I intended. ruby stores the binding of the proc in the context it was created and hence it rightly reports that self is "Human" even though it is being called by Developer.

Go to http://facets.rubyforge.org/apidoc/api/core/index.html and look for instance_exec method. The doc says

Evaluate the block with the given arguments within the context of this object, so self is set to the method receiver.

It means that instance_exec evaluates self in a new context. Now try the same code with instance_exec .

module Kernel
  def singleton_class
    class << self
      self
    end
  end
end

class Human
  proc = lambda { puts 'proc says my class is ' + self.name.to_s }

  singleton_class.instance_eval do
    define_method(:lab)  do
      self.instance_exec &proc
    end
  end
end

class Developer < Human
end

Human.lab # class is Human
Developer.lab # class is Developer

In this case Developer.lab says Developer and not Human.

You can also checkout this page which has much more detailed explanation of instance_exec and also emphasizes that instance_exec does pass a new value of self .

instance_exec is so useful that ActiveSupport needs it. And since ruby 1.8.6 does not have it ActiveSupport has code to support it.

I came across instance_exec issue while resolving #4507 rails ticket . The final solution did not need instance_exec but I learned a bit about it.

 
Neeraj Singh's profile picture

Comments