From: Ken Bloom Date: 2006-09-09T01:40:18+09:00 Subject: Re: [CHALLENGE] better alias_method 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: > > module Patchable > module ClassMethods > def patch(method_name = nil, &new_body) > if method_name > method_name = method_name.to_sym > pre_patched_versions[method_name] = instance_method > (method_name) > 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| > pre_patched_versions[m.to_sym] = instance_method(m) > 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 > self.class.pre_patched_versions[meth_name].bind(self).call > (*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 > hyper + 'b' > end > end > > p new.m > > patch do > def m > hyper + 'c' > end > end > > #p new.m doesn't work, infinite recursion > end > > 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/