Search code examples
rubyscopeblockprocinstance-eval

Ruby Block Scope with instance_eval


My understanding of Ruby blocks and procs was that they are all closures. Now that I've seen it in use with instance_eval, I'm a little confused. What is the magic-sauce, the under workings when looking at the bare metal, that changes how a block's scope behaves under most common uses when compared to use with instance_eval?

Here is an example you can dump in IRB to see what I mean. I've included both a proc.call and block yield version example. Happily, they both behave the same way.

# Testing block/proc and eval
class Example
  def initialize(value)
    # value defined in the instance
    @value = value
  end

  def call_a_proc(proc)
    proc.call self
  end

  def yield_to_block
    yield self
  end
end

# Value defined in the global object
@value = 1
example = Example.new 'a'

# the block/proc that prints @value
proc1 = -> instance { puts @value }

# instance calling/yielding the block/proc that prints @value
proc2 = -> instance { instance.call_a_proc proc1 }
proc3 = -> instance { instance.yield_to_block &proc1 }

# instance_eval of the block that prints @value
proc4 = -> instance { instance.instance_eval &proc1 }

# the block/proc reference @value from the global context, the context in which it was defined (standard closure)
example.call_a_proc proc1
example.yield_to_block &proc1
example.call_a_proc proc2
example.yield_to_block &proc2
example.call_a_proc proc3
example.yield_to_block &proc3

# block/proc has it's context redefined as coming from within the instance.
example.call_a_proc proc4
example.yield_to_block &proc4

I understand that this is the point of the instance_eval method, I'm just not exactly sure how it works.


Solution

  • When you define @value in the lexical scope (your main source file), you're defining an instance variable in the global interpreter. For example:

    self #=> main
    # "self" here refers to the main interpreter, which is of class Object
    self.instance_variable_get(:@value) #=> 1
    # "example" is your instance above
    example.instance_variable_get(:@value) #=> "a"
    # "self" has been changed to refer to "example" using instance_eval
    example.instance_eval { self.instance_variable_get(:@value) } #=> "a"
    # this syntax is just a shortcut for the line above
    example.instance_eval { @value } #=> "a"
    

    With instance_eval, all you're doing is replacing the main interpreter self with the object you've called instance_eval on.