Calling non-existing methods works! (little magic??)

M

Martin S. Weber

Hoi Rubyists.

I am kind of ... confused. Up till now I had the illusion you could only
call methods on an object which you could also find via methods or via
the included modules. Fine... Why does this output what it does then?
Where is this method defined? Some kind of method lookup cache? How to
force flushing it (besides redefining the methods of the receiver?)

Thanks in advance for the help,

-Martin

--- snippiesnap: fun_delegate.rb ---
class A; def a; "a"; end; end
class B; def b; "b"; end; end

# obvious: return difference of instance methods, ignoring "method_missing"
def inst_meth_delta( klass1, klass2 )
klass1.instance_methods - klass2.instance_methods - [ "method_missing" ]
end


# obvious:p
# if delegation is to be established, create an anonymous module, define
# methods on it to delegate to the receiver, extend the sender with this
# module.
# if delegation is to be deleted, remove methods from the anonymous module.
def fun_delegate(snd, rcv)
return unless (snd && rcv)

deinstalled=false;
# maybe remove the delegation again?
(class << snd ; included_modules ; end).each do |mod|
# get the source object_id (obj which is delegated to)
dd = mod.instance_variables.include?('@dd') && mod.instance_variable_get:)@dd)
# deinstall methods if rcv is delegated to a second
# time - or when rcv is :eek:ff.
if dd && (dd == rcv.object_id || rcv == :eek:ff) then
puts "Removing delegation from #{snd} to #{rcv} (mod: #{mod})"
mod.instance_methods.each do |meth|
mod.send:)remove_method, meth.intern)
end
deinstalled=true;
mod.instance_variable_set:)@dd, nil)
end
end

# ... or set it up?
unless deinstalled
(mod = Module.new).instance_variable_set:)@dd, rcv.object_id)
puts "delegating from #{snd} to #{rcv} via #{mod}..."
inst_meth_delta(rcv.class, snd.class).each {|meth|
mod.send:)define_method, meth.intern) {|*args| rcv.send(meth.intern, *args) }
}
snd.extend(mod)
end

end

a=A.new; b=B.new

# obvious so far
# nometherr
begin puts a.b rescue puts $! end
fun_delegate a,b
# "b"
begin puts a.b rescue puts $! end
fun_delegate a,b
# MAGIC!!!
# "b"
begin puts a.b rescue puts $! end
# Fine, where is this "b" coming from?
puts a.methods.find{|m| m=='b'} ;# -> nil
puts a.methods - Kernel.methods ;# -> "a"
puts a.singleton_methods ;# -> []
puts "#{(class << a ; included_modules ; end).collect {|mod|
mod.instance_methods unless mod.name == "Kernel"
}}" ;# -> [[], nil]
--- snappiesnip: fun_delegate.rb ---

--- output ---
$ ruby -vw fun_delegate.rb
ruby 1.8.4 (2005-12-24) [i386-netbsdelf]
undefined method `b' for #<A:0x806b618>
delegating from #<A:0x806b618> to #<B:0x806b604> via #<Module:0x806b4b0>...
b
Removing delegation from #<A:0x806b618> to #<B:0x806b604> (mod: #<Module:0x806b4b0>)
b
nil
a

--- end ---
 
P

Pit Capitain

Martin said:
I am kind of ... confused. Up till now I had the illusion you could only
call methods on an object which you could also find via methods or via
the included modules. Fine... Why does this output what it does then?
Where is this method defined? Some kind of method lookup cache? How to
force flushing it (besides redefining the methods of the receiver?)
(...)

Martin, this seems to be a bug. As you guessed, there is a method lookup
cache, and it isn't flushed correctly when removing a method. Here's a
simpler code to show the bug...

Define a module, include it in a class and call a method of the module:

module M
def m
p "M#m"
end
end

class C
include M
end

C.new.m
# => "M#m"

Remove the method from the module:

module M
remove_method :m
end

The method lookup cache of class C isn't cleared (bug), so you can still
call the old method:

C.new.m
# => "M#m"

Here's one way to clear the method lookup cache for a given method:
create an anonymous module and define a method with the same name:

Module.new do
def m
end
end

C.new.m rescue p $!
# => #<NoMethodError: undefined method `m' for #<C:0x2b83d10>>

I will forward this to the ruby-core mailing list.

Thanks for the bug report,
Pit
 
M

Mauricio Fernandez

Martin, this seems to be a bug. As you guessed, there is a method lookup
cache, and it isn't flushed correctly when removing a method. Here's a
simpler code to show the bug... [...]
The method lookup cache of class C isn't cleared (bug), so you can still
call the old method:

C.new.m
# => "M#m" [...]
I will forward this to the ruby-core mailing list.

This was fixed one month ago in both HEAD and ruby_1_8:

* eval.c (rb_clear_cache_for_undef): clear entries for included
module. fixed: [ruby-core:08180]
 
P

Pit Capitain

Mauricio said:
This was fixed one month ago in both HEAD and ruby_1_8:

* eval.c (rb_clear_cache_for_undef): clear entries for included
module. fixed: [ruby-core:08180]

Thanks for the info, Mauricio.

Regards,
Pit
 

Ask a Question

Want to reply to this thread or ask your own question?

You'll need to choose a username for the site, which only take a couple of moments. After that, you can post your question and our members will help you out.

Ask a Question

Members online

No members online now.

Forum statistics

Threads
473,770
Messages
2,569,583
Members
45,075
Latest member
MakersCBDBloodSupport

Latest Threads

Top