Search code examples
rubyblock

Changing the scope of a captured block in Ruby


I want to capture blocks (with an associated name), but without any change in the scope in which they are written. The code below has two ways of capturing a block (capt_a and capt_b). capt_a works fine and I'd like capt_b to work the same way. Is it possible to modify capt_b so that the effect is the same as capt_a?

class Capturer
  attr_reader :method, :block

  def capt_a
    yield self
    self
  end

  def capt_b(&block)
    instance_eval(&block)
    self
  end

  def method_missing(method, &block)
    @method = method
    @block = block
  end
end

# Example:
a = Capturer.new.capt_a{|capt| capt.foo{self} }.block
b = Capturer.new.capt_b{ foo{self} }.block

a.call # => main
b.call # => #<Capturer:0x000001008fb5c8 @method=:foo, @block=#<Proc:[email protected]:23>>
       # I would like 'main'

Solution

  • After some research in the direction @bioneuralnet suggested, it's possible to create a new Proc doing a new instance_eval to restore the context. The binding of the initial block is used to get the initial self. So here is a (somewhat ugly) solution:

      def capture_b(&block)
        instance_eval(&block)
        the_desired_self = block.binding.eval("self")
        bk = @block
        @block = Proc.new{ the_desired_self.instance_eval(&bk) }
        self
      end
    

    It's not perfect, as it will be slower and since the original block won't be == to the resulting block; maybe there's a better solution?