[lnkForumImage]
TotalShareware - Download Free Software

Confronta i prezzi di migliaia di prodotti.
Asp Forum
 Home | Login | Register | Search 


 

Forums >

comp.lang.ruby

[CHALLENGE] better alias_method

Ara.T.Howard

9/8/2006 5:35:00 AM

23 Answers

Logan Capaldo

9/8/2006 2:09:00 PM

0


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 ;)




> -a
> --
> what science finds to be nonexistent, we must accept as
> nonexistent; but what
> science merely does not find is a completely different matter... it
> is quite
> clear that there are many, many mysterious things.
> - h.h. the 14th dalai lama
>


Ken Bloom

9/8/2006 2:50:00 PM

0

On Fri, 08 Sep 2006 14:34:39 +0900, ara.t.howard 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

It would be nice to have this built in to the language -- a way to blur
the difference between overriding an inherited method and overriding a
method defined on the current class, so that `super' would call whatever
the previous version of the was.

Try this idea on for size. I wrote it a few days ago. Look at the
unit tests at the end to see how it's intended to be used. I think a
little syntactic sugar would be nice, but it gets the job done.

class Module
private
def override(method_name,&block)
begin
#case 1: I'm overriding a method defined on this object
old_method=instance_method(method_name)
alias_for_old=unused_alias
alias_method alias_for_old, method_name
rescue NameError
begin
#case 2: I'm overriding a method that I inherit from
old_method=superclass.instance_method(method_name)
rescue NameError
#case 3: I'm not overriding anything. Just use a simple
#function that we can call without side effects
old_method=Object.instance_method(:nil?)
end
end
define_method(method_name) do |*args|
block.call(old_method.bind(self),*args)
end
end
def unused_alias
newalias=nil
while newalias==nil or instance_methods.include?(newalias)
newalias=:"__kenoverride__#{rand(10**20)}__"
end
newalias
end
end

if __FILE__==$0
require 'test/unit'
class TestOverride < Test::Unit::TestCase
def setup
eval <<-"end;"
class A
def foo; "a"; end
end
end;
end


def test_alias
assert_nothing_raised do
eval <<-"end;"
class A
override(:foo){|old| old.call+"b"}
end
end;
end

assert_equal A.new.foo,"ab"

end

def test_inherit
assert_nothing_raised do
eval <<-"end;"
class B<A
override(:foo){|old| "c"+old.call }
end
end;
end

#I don't want to rely on whether test_inherit or test_alias
#comes first, so I'll test for the results of both orders.
assert ["ca","cab"].include?(B.new.foo)
end

#I'm not really sure whether this is the appropriate behavior
#or whether throwing an exception is more appropriate.
def test_neither
assert_nothing_raised do
eval <<-"end;"
class C
override(:foo){|old| "d" }
end
end;
end

assert_equal C.new.foo,"d"
end
end
end


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

Ken Bloom

9/8/2006 3:09:00 PM

0

On Fri, 08 Sep 2006 14:50:22 +0000, Ken Bloom wrote:

> On Fri, 08 Sep 2006 14:34:39 +0900, ara.t.howard 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
>
> It would be nice to have this built in to the language -- a way to blur
> the difference between overriding an inherited method and overriding a
> method defined on the current class, so that `super' would call whatever
> the previous version of the was.
>
> Try this idea on for size. I wrote it a few days ago. Look at the
> unit tests at the end to see how it's intended to be used. I think a
> little syntactic sugar would be nice, but it gets the job done.
>
> class Module
> private
> def override(method_name,&block)
> begin
> #case 1: I'm overriding a method defined on this object
> old_method=instance_method(method_name)
> alias_for_old=unused_alias
> alias_method alias_for_old, method_name
> rescue NameError
> begin
> #case 2: I'm overriding a method that I inherit from
> old_method=superclass.instance_method(method_name)
> rescue NameError
> #case 3: I'm not overriding anything. Just use a simple
> #function that we can call without side effects
> old_method=Object.instance_method(:nil?)
> end
> end
> define_method(method_name) do |*args|
> block.call(old_method.bind(self),*args)
> end
> end
> def unused_alias
> newalias=nil
> while newalias==nil or instance_methods.include?(newalias)
> newalias=:"__kenoverride__#{rand(10**20)}__"
> end
> newalias
> end
> end
>
> if __FILE__==$0
> require 'test/unit'
> class TestOverride < Test::Unit::TestCase
> def setup
> eval <<-"end;"
> class A
> def foo; "a"; end
> end
> end;
> end
>
>
> def test_alias
> assert_nothing_raised do
> eval <<-"end;"
> class A
> override(:foo){|old| old.call+"b"}
> end
> end;
> end
>
> assert_equal A.new.foo,"ab"
>
> end
>
> def test_inherit
> assert_nothing_raised do
> eval <<-"end;"
> class B<A
> override(:foo){|old| "c"+old.call }
> end
> end;
> end
>
> #I don't want to rely on whether test_inherit or test_alias
> #comes first, so I'll test for the results of both orders.
> assert ["ca","cab"].include?(B.new.foo)
> end
>
> #I'm not really sure whether this is the appropriate behavior
> #or whether throwing an exception is more appropriate.
> def test_neither
> assert_nothing_raised do
> eval <<-"end;"
> class C
> override(:foo){|old| "d" }
> end
> end;
> end
>
> assert_equal C.new.foo,"d"
> end
> end
> end
>
>

One more unit test that this passes. (This one seems to go to the heart of
the issue you guys are having.)

def test_longchain
assert_nothing_raised do
eval 'class D; def foo; "a"; end; end'
end

assert_equal "a", D.new.foo

10.times do |n|
assert_nothing_raised do
eval 'class D; override :foo do |old| old.call+"a"; end; end'
end

assert_equal "a"*(n+2), D.new.foo
end

end



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

Ara.T.Howard

9/8/2006 3:18:00 PM

0

Ara.T.Howard

9/8/2006 3:21:00 PM

0

Jason Nordwick

9/8/2006 3:50:00 PM

0

The best alias_method methods would be one that didn't exist.

(1) It encourages breaking of naming conventions since at times two functions get defined, one with the convension and one without, (see hash.merge! and hash.update), and I really liked the convention where mutatations have bangs at the end.

(2) This one function double the complexity of learning to read ruby. Now I don't just have to know what hash.update does, but also that hash.merge! is the same thing. And when individual developers start adding aliases to buildins things get ever worse. Array.lower isn't necessary when Array.first and Array[0] are both as short or shorter and just as descriptive.

Alaises increase complexity and really are one of Ruby's biggest dislikes for me.

-j



Ara.T.Howard

9/8/2006 4:18:00 PM

0

Ara.T.Howard

9/8/2006 4:24:00 PM

0

Ken Bloom

9/8/2006 4:25:00 PM

0

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...

Ken Bloom

9/8/2006 4:39:00 PM

0

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...