Yet another way to wrap methods in Ruby
Here's another way to wrap methods, reminiscent of "cuts". It can be used to do things like
class Foo def foo; "Foo#foo" end end class B < Foo def foo; "B#foo " + super end end class C < B def foo; "C#foo " + super end end b = B.new c = C.new b.foo # => "B#foo Foo#foo" c.foo # => "C#foo B#foo Foo#foo" Foo.replace{ def foo; "New#foo [" + super + "]" end } b.foo # => "B#foo New#foo [Foo#foo]" c.foo # => "C#foo B#foo New#foo [Foo#foo]" B.replace{ def foo; "<" + super + ">" end } b.foo # => "<B#foo New#foo [Foo#foo]>" c.foo # => "C#foo <B#foo New#foo [Foo#foo]>" Foo.replace{ def foo; "New2#foo [" + super + "]" end } b.foo # => "<B#foo New2#foo [Foo#foo]>" c.foo # => "C#foo <B#foo New2#foo [Foo#foo]>"
My implementation uses a special class which is made right after the user creates a new class, and derives from the latter: in that class, super will correspond to the original method definition. This means that, in the above example, the klass chain would look like this:
c.class.ancestors # => [C, C, B, B, Foo, Foo, Object, Kernel]
The second C, B, Foo correspond to the original classes created by doing
class Foo; ... end
and hold the original method definitions. The preceding classes are created right after, and the Object::{B,C,Foo} constants are redefined so that everything looks as if the original class had been replaced by a new one. The proxy classes are created as follows:
Klass = Object.new def Klass.new(superklass = Object) #sup = Class.new(superklass) #klass = Class.new(sup) klass = Class.new(superklass) klass.extend RefinableClass klass end
The corresponding constants are redefined with
class Class # just in case it was doing something interesting old_inherited = instance_method(:inherited) define_method(:inherited) do |child| name = child.to_s unless /#</ =~ name nparts = name.split(/::/) klass = nparts.inject(Object){|s,x| s.const_get(x)} eval("lambda{|x| ::#{name} = x}").call(Klass.new(klass)) #a cleaner way: #last = nparts.pop #target = nparts.inject(Object){|s,x| s.const_get(x)} #target.module_eval{ remove_const(last) } #target.const_set(last, Klass.new(klass)) end old_inherited.bind(self).call(child) end end
The superclass can be used to hold another method definition when there was none originally, as shown in the following example:
class Y end class Y2 < Y def foo; "Y2#foo" end end y = Y2.new y.foo # => "Y2#foo" class Y2 m = instance_method(:foo) Y.class_eval{ define_method(:foo, m) } def foo; "new Y2#foo <" + super + ">" end # !> method redefined; discarding old foo end y.foo # => "new Y2#foo <Y2#foo>"
The code that actually allows one to wrap (once) a method doesn't look that good due to the "2 definitions per method" semantics (since it has to push the former definition to the superclass if it wasn't defined there before).
module RefinableClass def replace(&b) @replaced ||= [] oldmethods = instance_methods(false) mpairs = instance_methods(false).map{|name| [name, instance_method(name)]} old_defs = Hash[*mpairs.flatten] # we remove and restore them later just to avoid the warnings oldmethods.each{|m| remove_method(m)} class_eval(&b) newmethods = instance_methods(false) @replaced.concat newmethods old_defs.each_pair do |name, mbody| define_method(name, mbody) unless newmethods.include? name end (newmethods & oldmethods).each do |m| next if @replaced.include? m superclass.class_eval{ define_method(m, old_defs[m]) } end end end
This was quite an interesting exercise, but the code feels too forceful, and there are several limitations. Modules look more promising (arbitrary number of advices, no Class#become-style magic)... I'll explore that later.
- 39 http://www.artima.com/forums/flat.jsp?forum=123&thread=139726
- 22 http://www.artima.com/buzz/community.jsp?forum=123
- 15 http://anarchaia.org
- 15 http://chneukirchen.org/anarchaia
- 7 http://www.anarchaia.org
- 6 http://anarchaia.org/archive/2005/12.html
- 2 http://chneukirchen.org/anarchaia/archive/2005/12/07.html
- 1 http://www.anarchaia.org/archive/2005/12.html
- 1 http://search.yahoo.com/search?p=instance_method ruby&prssweb=Search&ei=UTF-8&fr=yfp-t-501&x=wrt
- 1 http://www.blingo.com/search?q=ruby wrap methods&sourceid=firefox&s=0&s=0
Keyword(s):[blog] [ruby] [method] [advice]
References:[Ruby]