eigenclass logo
MAIN  Index  Search  Changes  PageRank  Login

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.


Last modified:2005/12/07 10:50:17
Keyword(s):[blog] [ruby] [method] [advice]
References:[Ruby]