Search code examples
rubyreflectionclosuresself

Ruby - define_method and closures


define_method exhibits the following behaviour:

class TestClass
  def exec_block(&block) ; yield ; end
end
TestClass.new.send(:exec_block) do ; puts self ; end
# -> main
TestClass.send(:define_method, :bing) do ; puts self ; end
TestClass.new.bing
# -> <TestClass:...>

What I do not understand is that the the block passed to define_method is supposed to be a closure. As such it should (at least according to my understanding) capture the value of self as main, as exhibited when calling exec_block.

I understand that the block will become the body of the method, yet I do not understand the reason for the behavior. Why does the block evaluate to different things when used with different methods ?

How can I reproduce the behavior of block with define_method for other methods ? i.e. how could I write exec_block to have it output <TestClass:...> instead of `main´ ?


Solution

  • self is captured by the closure like any other variable. We can verify that by passing a Proc around different object instances:

    class A
      def exec_block(&block)
        block.call
      end
    end
    
    class B
      def exec_indirect(&block)
        A.new.exec_block(&block)
      end
    end
    
    block = proc { p self }
    a = A.new; b = B.new
    
    a.exec_block(&block)    # => main
    b.exec_indirect(&block) # => main
    

    However, BasicObject#instance_eval and the alikes rebind the self variable dynamically:

    In order to set the context, the variable self is set to obj while the code is executing, giving the code access to obj’s instance variables

    Module#define_method in turn uses instance_eval to execute the associated block:

    If a block is specified, it is used as the method body. This block is evaluated using instance_eval [...]

    Observe:

    A.send(:define_method, :foo, &block)
    a.foo                   # => #<A:0x00000001717040>
    a.instance_eval(&block) # => #<A:0x00000001717040>
    

    With that knowledge, you can now rewrite your exec_block to use instance_eval:

    class A
      def exec_block(&block)
        instance_eval(&block)
      end
    end
    
    block = proc { p self }
    A.new.exec_block(&block)  # => #<A:0x00000001bb9828>
    

    As mentioned before, using instance_eval seems to be the only way to run a Proc instance with a modified context. It can be used to implement dynamic binding in Ruby.