Frost-safe DSL'ing with instance_exec
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
Keyword(s):[blog] [ruby] [instance_exec] [dsl] [frozen]
References:[Not quite a SuperStruct, maybe a SuperClass? automatic attributes and initialization] [Ruby] [Symbols, meta-programming and leaks. On harakiri and DoSing Rails?] [The dangers of #undef_method, #instance_exec recalled for memleaking!]