eigenclass logo
MAIN  Index  Search  Changes  PageRank  Login

Frost-safe DSL'ing with instance_exec

/hiki/instance_exec/update.png A bounded-space #instance_exec implementation is available.

Object#instance_eval is often of use when creating a DSL with Ruby, but it's not as powerful as the Object#instance_exec method introduced in the 1.9 branch, which can be used as in

o = Struct.new(:val).new(1)
o.instance_exec(1){|arg| val + arg }               # => 2

i.e. it allows you to pass arguments to the block which is to be evaluated with a new self*1.

This is one of the 1.9 features I wouldn't mind having in 1.8.

My first implementation, posted to ruby-talk, was

class Object
  def instance_exec(*args, &block)
    mname = "__instance_exec_#{Thread.current.object_id.abs}"
    class << self; self end.class_eval{ define_method(mname, &block) }
    begin
      ret = send(mname, *args)
    ensure
      class << self; self end.class_eval{ undef_method(mname) } rescue nil
    end
    ret
  end
end
    

It operates by defining a temporary method in the singleton class of the object to be used as the new self inside the block, easily passing the basic test provided by Jim Weirich in ruby-talk:179038:

class Dummy
  def f
    :dummy_value
  end
end

require 'test/unit'
class TestInstanceEvalWithArgs < Test::Unit::TestCase
  def test_instance_exec
    # Create a block that returns the value of an argument and a value
    # of a method call to +self+.  
    block = lambda { |a| [a, f] }

    assert_equal [:arg_value, :dummy_value], 
      Dummy.new.instance_exec(:arg_value, &block)
  end
end

#>> Loaded suite -
#>> Started
#>> .
#>> Finished in 0.000566 seconds.
#>> 
#>> 1 tests, 1 assertions, 0 failures, 0 errors

That instance_exec implementation is thread-safe thanks to the Thread.current.object_id trick, but it doesn't work with immediate values (Fixnums and friends), and most importantly it bombs when given a frozen object:

class TestInstanceEvalWithArgs 
  def test_instance_exec_with_frozen_obj
    block = lambda { |a| [a, f] }

    obj = Dummy.new
    obj.freeze
    assert_equal [:arg_value, :dummy_value],
      obj.instance_exec(:arg_value, &block)
  end
end
#>> Loaded suite -
#>> Started
#>> .E
#>> Finished in 0.000831 seconds.
#>> 
#>>   1) Error:
#>> test_instance_exec_with_frozen_obj(TestInstanceEvalWithArgs):
#>> TypeError: can't modify frozen class/module
#>>     -:9:in `define_method'
#>>     -:9:in `instance_exec'
#>>     -:9:in `instance_exec'
#>>     -:59:in `test_instance_exec_with_frozen_obj'
#>> 
#>> 2 tests, 1 assertions, 0 failures, 1 errors

At that point I remembered there had been some discussion on ruby-core regarding instance_exec. A quick google search showed that there's a number of implementations floating around. Funnily enough, my implementation was very similar to Ara Howard's, posted in the original ruby core thread*2.

I also discovered that Rails ships with its own instance_exec in active_support:

class Proc 
  def bind(object)
    block, time = self, Time.now
    (class << object; self end).class_eval do
      method_name = "__bind_#{time.to_i}_#{time.usec}"
      define_method(method_name, &block)
      method = instance_method(method_name)
      remove_method(method_name)
      method
    end.bind(object)
  end
end

class Object
  def instance_exec(*arguments, &block)
    block.bind(self)[*arguments]
  end
end

That definition is essentially equivalent to the other ones, but not strictly thread-safe, though (it will fail if your box or your Ruby interpreter are really fast ;), and when you decide to mess up with the system clock).

I also found Facets mentioned in that context, but was too lazy to find the instance_exec equivalent amongst its 400+ methods(!) (or maybe it was never added actually?).

Implementing a frost-safe #instance_exec


Instead of defining the temporary method inside the singleton class (which might not exist), or the class of the receiver (which might also be frozen), why not create a warm nest for our tiny methods?

class Object
  module InstanceExecHelper; end
  include InstanceExecHelper
  def instance_exec(*args, &block) # !> method redefined; discarding old instance_exec
    mname = "__instance_exec_#{Thread.current.object_id.abs}_#{object_id.abs}"
    InstanceExecHelper.module_eval{ define_method(mname, &block) }
    begin
      ret = send(mname, *args)
    ensure
      InstanceExecHelper.module_eval{ undef_method(mname) } rescue nil
    end
    ret
  end
end

Victory:

class TestInstanceEvalWithArgs 
  def test_instance_exec_nested
    i = 0
    obj = Dummy.new
    block = lambda do |arg|
      [arg] + instance_exec(1){|a| [f, a] }
    end
    
    # the following assertion expanded by the xmp filter automagically from:
    # obj.instance_exec(:arg_value, &block) #=>
    assert_equal([:arg_value, :dummy_value, 1], obj.instance_exec(:arg_value, &block))

  end
end
# >> Loaded suite -
# >> Started
# >> ...
# >> Finished in 0.002261 seconds.
# >> 
# >> 3 tests, 3 assertions, 0 failures, 0 errors

うわぁ moduleの中 すごくあたたかい *3


trans 2006-07-07 (Fri) 13:04:33

Yep. I never did add this to Facets. Was using the it in some embedded context and had forgotten about it. Anyway coll thread safe version. Here's what I put in Facets:

module Kernel

 # Like instace_eval but allows parameters to be passed.

  def instance_exec(*args, &block)
    mname = "__instance_exec_#{Thread.current.object_id.abs}_#{object_id.abs}"
    Object.class_eval{ define_method(mname, &block) }
    begin
      ret = send(mname, *args)
    ensure
      Object.class_eval{ undef_method(mname) } rescue nil
    end
    ret
  end

end

Thanks!

mfp 2006-07-09 (Sun) 02:27:50

My first contribution to Facets? :)

*1 instance_eval actually passes an argument to the block, the new value ofself

*2 I believe Ara's implementation wouldn't work for Class objects though, and it doesn't address the fact that object_ids have become negative, at least on my platform (x86), as of late.

*3 reminded me of http://www.namikilab.tuat.ac.jp/~sasada/diary/200601.html#cc20-1