On Fri, 08 Sep 2006 23:09:07 +0900, Logan Capaldo wrote:

> On Sep 8, 2006, at 1:34 AM, ara.t.howard / noaa.gov wrote:
> 
>>
>> i've been wanting a better alias_method for quite some time.   
>> essentially i'd
>> like a way out of the trap where executing
>>
>>   alias_method '__fubar__', 'fubar'
>>
>> goes haywire when __fubar__ already exists.  we've all seen it  
>> happen before.
>>
>> anyhow.  the interface it'd like would be
>>
>>
>>   class C
>>     def m() 'a' end
>>
>>     push_method 'm'
>>
>>     def m() super + 'b' end
>>   end
>>
>>   p C.new.m  #=> 'ab'
>>
>>
>> what i've got is quite close, but no cigar.  it has a fundemental  
>> problem with
>> the way ruby scopes super which i'm too tired atttm to figure out.   
>> i'm hoping
>> i can go to bed and wake up to a nice patch ;-)  here's what i've got:
>>
>>
>>     harp:~ > cat a.rb
>>     class Module
>>       def push_method m
>>         this = self
>>
>>         include Module.new{
>>           @m = this.instance_method m
>>
>>           this.module_eval{ remove_method m }
>>
>>           module_eval <<-code
>>             def #{ m }(*a, &b)
>>               um = ObjectSpace._id2ref #{ @m.object_id }
>>               um.bind(self).call *a, &b
>>             end
>>           code
>>         }
>>       end
>>     end
>>
>>     class C
>>       def m
>>         'a'
>>       end
>>       p new.m #=> 'a'
>>
>>
>>       push_method 'm'
>>
>>
>>       def m
>>         super + 'b'
>>       end
>>       p new.m #=> 'ab'
>>
>>
>>       push_method 'm'
>>
>>
>>       def m
>>         super + 'c'
>>       end
>>       p new.m #=> 'abc'
>>     end
>>
>>
>>
>>     harp :~ > ruby a.rb
>>     "a"
>>     "ab"
>>     a.rb:31:in `m': stack level too deep (SystemStackError)
>>             from (eval):3:in `m'
>>             from a.rb:31:in `m'
>>             from (eval):3:in `m'
>>             from a.rb:31:in `m'
>>             from (eval):3:in `m'
>>             from a.rb:31:in `m'
>>             from (eval):3:in `m'
>>             from a.rb:31:in `m'
>>              ... 2343 levels...
>>             from a.rb:31:in `m'
>>             from (eval):3:in `m'
>>             from a.rb:40:in `m'
>>             from a.rb:42
>>
>>
>> have at it - i'll be back in 8 hrs.  ;-)
>>
>>
> 
> I have this, it takes a different approach though:
> 
[Code snipped. You'll have to look above if you want to know how the
broken code works]
> 
> Darn. You seem to have made me discover a bug in my impl. It doesn't
> work for more than one level of patching per method. Well maybe someone
> will give me a patch too ;)

caller knows a function only by the name you call it.

hence

class A
  def hyper
    p caller
  end

  def a
    hyper
  end
  alias_method :b,:a
end

a=A.new
puts "Calling by a"
a.a
puts "Calling by b"
a.b

gives:
Calling by a
["(irb):7:in `a'", "(irb):14:in `irb_binding'", "/usr/lib/ruby/1.8/irb/workspace.rb:52:in `irb_binding'", ":0"]
Calling by b
["(irb):7:in `b'", "(irb):16:in `irb_binding'", "/usr/lib/ruby/1.8/irb/workspace.rb:52:in `irb_binding'", ":0"]

In your code, we need to do several things:
* rename and call them by their new names to prevent infinite recursion
* keep track of pre_patched_versions correctly when their name changes

So here's my fix for your code, to incorporate these ideas.
I think that if anyone uses straight up alias_method on methods that we
are patching, things will break, so it's probably a good idea to figure out
how to fix that too.


module Patchable
   module ClassMethods
     def unused_alias
       newalias=nil
       while newalias==nil or instance_methods.include?(newalias)
	 newalias=:"__kenoverride__#{rand(10**20)}__"
       end
       newalias
     end

     def patch(method_name = nil, &new_body)
       if method_name
         method_name = method_name.to_sym
	 w=unused_alias
	 alias_method w,method_name
	 remove_method method_name
	 pre_patched_versions[w]=pre_patched_versions[method_name.to_sym]\
	   if pre_patched_versions.include?(method_name.to_sym)
         pre_patched_versions[method_name.to_sym] = w
         define_method(method_name, &new_body)
       else
         klass = Class.new
         imeths = klass.instance_methods
         klass.class_eval(&new_body)
         new_meths = klass.instance_methods - imeths
         new_meths.each do |m|
	   w=unused_alias
	   alias_method w,m
	   remove_method m
	   pre_patched_versions[w]=pre_patched_versions[m.to_sym]\
	     if pre_patched_versions.include?(m.to_sym)
           pre_patched_versions[m.to_sym] = w
         end
         class_eval(&new_body)
       end
       self
     end

     def pre_patched_versions
       @pre_patched_versions ||= {}
     end
   end

   def hyper(*args, &block)
     meth_name = caller[0][/`([^']+)'/, 1].to_sym
     tocall=self.class.pre_patched_versions[meth_name]
     send(tocall,*args, &block)
   end

   def self.included(other)
     other.extend(ClassMethods)
   end
end


class C
   include Patchable

   def m
     'a'
   end

   p new.m
   patch do
     def m
       raise StandardError if caller.length>10
       hyper + 'b'
     end
   end

   p new.m

   patch do
     def m
       raise StandardError if caller.length>10
       hyper + 'c'
     end
   end

   p new.m

   #you didn't have a test case for this, but I added it

   patch :m do hyper+'d' end
   
   p new.m

end


-- 
Ken Bloom. PhD candidate. Linguistic Cognition Laboratory.
Department of Computer Science. Illinois Institute of Technology.
http://www.iit.edu/~kbloom1/