Method resolution in Ruby : OR how to find Mr. Right method?

Before I begin: Some of the stuff below may be incorrect (Corrections welcome.), however it helps me understand ruby method resolution. I have come up with this model based on my reading of the RubyProgrammingLanguage book (must-read for anyone learning Ruby) by Flanagan & Matz and some tinkering.. it may not be accurate (if that is what you're after)


Basket of Facts:
  • A Ruby type is an object of type 'Class' (henceforth referred to as a class-object)
  • An object has a class attribute pointing to the right class-object.
  • An object has an "eigenclass"/"metaclass" that holds singleton methods (methods that are only available to that instance.)

class-objects are special in a few ways:
  • a class-object has its class attribute pointing to the 'Class' type in Ruby
  • a class-object has a superclass
  • the "eigenclass" of a class-object also has a superclass; the eigenclass of its superclass
  • a class-object has an included modules list
  • a class-object has a table of methods (instance methods defined in the class definition)
  • class methods are singleton methods of the class-object
  • a class-object is generally associated with a constant viz. it's class name.



So now let's get started. Don't let the following mouse-drawn picture scare you - it'll all make (some?) sense in sometime. In the meantime, right click on it and open it up in a new tab and have the picture handy for reference while you descend into this post.



Next jump right over this big puddle of source code. You can copy it into your editor if you want to try it out with me..

module Kernel
def breadcrumb
puts "Kernel#breadcrumb"
Module.nesting[0].instance_eval{ remove_method :breadcrumb }
end
end
class Object
def breadcrumb
puts "Object#breadcrumb"
Module.nesting[0].instance_eval{ remove_method :breadcrumb }
end
def self.breadcrumb
puts "Object.breadcrumb"
class << Module.nesting[0]
instance_eval { remove_method :breadcrumb }
end
end
end

module MyModule
def breadcrumb
puts "MyModule#breadcrumb"
Module.nesting[0].instance_eval{ remove_method :breadcrumb }
end

end

class Base
MY_CONST = :b
include MyModule

def breadcrumb
puts "Base#breadcrumb";
Base.class_eval{ remove_method :breadcrumb }
# Base.instance_eval{ remove_method :breadcrumb } UH-OH! why does this remove the inst method
end
def self.breadcrumb
puts "Base.breadcrumb"
class << Module.nesting[0]
instance_eval{ remove_method :breadcrumb}
end
end
end

class Derived < Base
def breadcrumb
puts "Derived#breadcrumb";
Module.nesting[0].instance_eval{ remove_method :breadcrumb }
end
def self.breadcrumb
puts "Derived.breadcrumb"
class << self
instance_eval{ remove_method :breadcrumb}
end
end
end

class Class
def breadcrumb
puts "Class#breadcrumb";
Class.class_eval{ remove_method :breadcrumb }
end
end
class Module
def breadcrumb
puts "Module#breadcrumb";
Module.class_eval{ remove_method :breadcrumb }
end
end



****LANDING ZONE****
Nice Jump!

I have defined a new class called 'Derived' which derives from 'Base' and includes 'MyModule'.
Unless a base type is explicitly specified, all classes derive from Object (which includes Kernel) (and derives from BasicObject - new in Ruby 1.9). e.g. Base derives from Object. This can be verified via


d = Derived.new
p d.class.ancestors # => [Derived, Base, MyModule, Object, Kernel, BasicObject]



Base and Derived each have defined a class method (self.breadcrumb) and an instance method (breadcrumb). MyModule just defines a module method of the same name.
Convention: Class/Singleton methods would print receiver.breadcrumb. Instance methods would print receiver#breadcrumb.

Take a deep breath and dive in...

First lets create a new instance of Derived and add a singleton method to it. (i.e. a method that is present only in that instance. Another instance of Derived would not have this method)
Here's how we do that...


d = Derived.new

class << d
def breadcrumb
puts "DerivedObj[id:%d].breadcrumb" % object_id
class << self
instance_eval{ remove_method :breadcrumb}
end
end
end


'Call once': once the method is called, the method disappears (is 'undefined'). This will help us uncover the order in Ruby tries to find a method. I have added such methods in all types up the hierarchy (and then some) to help us spot the trail.
(Note: Instance methods use class_eval to remove themselves. Singleton/class methods use instance_eval. Refer to the RPL book)

And now for the moment of truth


puts "Method call on an object".center(40, "*")
6.times{ d.breadcrumb }


Output:
********Method call on an object********
DerivedObj[id:6186928].breadcrumb
Derived#breadcrumb
Base#breadcrumb
MyModule#breadcrumb
Object#breadcrumb
Kernel#breadcrumb


Refer to the image (I was reading Why's Nobody know Shoes; which might explain some of the weird imagery and the helpfulness of it all ;) So here's what happened in the sequence illustrated by our 'call-once' breadcrumbs:

To the extreme left is our object d,
  • Call1: Find the eigenclass of d, look for a singleton method of that name. Found!
  • Call2: .. not found in eigenclass, find the class-object of d via the class attribute. Look in the methods defined in class Derived. Found!
  • Call3: .. not found in eigenclass/class. Look in included modules of D (most recently included one first).. there are none. Travel via the superclass portal to reach Base. Look for inst method. Found!
  • Call4: .. not found in eigen/class/incl module/superclass. Try included modules of superclass e.g. MyModule. Found!
  • Call5: .. not found in eigen/class-n-its-modules/superclass-n-its-modules. Go down the superclass portal again to reach Object. Look at instance methods. Found!
  • Call6: .. look in incl. modules of Object. Kernel. Found!



If you try calling the method again, Ruby would not be able to find a method of that name and would now try looking for a method_missing method (in the same manner as it tried looking for breadcrumb. However this time it is guaranteed to find it because Kernel has a method_missing implementation that raises the NoMethodError. Need to see it to believe it?


temp.rb:84:in `block in
': undefined method `breadcrumb' for # (NoMethodError)
from temp.rb:84:in `times'
from temp.rb:84:in `
'


So lets recap the steps.
1. Look in the eigenclass (golden arrow) for the class
2. If not found, traverse the class attribute (purple arrow) to find the class-object.
3. Look in the instance methods for this class
4. If not found, try included modules of the class (in reverse order of mixin)
5. If not found, check if we have a superclass (pink arrow). If yes, repeat steps 3-5 till we run out of superclasses
6. If still not found, go back to Step1 but this time look for a 'method_missing' method'


By the way, we just saw how instance methods are inherited. (Snuck that one on you.) If Derived doesn't define an instance method, all its ancestors are looked up in order ; inheritance works.

Take a break, have some lemonade before we set out again... Ready? Off we go again. This time instead of calling a method on an object, we call a class method. Comment the previous 2 lines...


puts "Method call on a class-object".center(40, "*")
7.times{ Derived.breadcrumb }


Output:
*****Method call on a class-object******
Derived.breadcrumb
Base.breadcrumb
Object.breadcrumb
Class#breadcrumb
Module#breadcrumb
Object#breadcrumb
Kernel#breadcrumb


Hey! we see some instance methods being invoked when we call a class method. What the... Deep breaths 1..2..3..pfffhoooo (u got to get the pfffhoooo right)

Let's see if our image can help us out, find the object pointed to by the constant Derived (3rd box from the left - first row)

  • Call1: Start at Derived. Follow the golden arrow to the Derived's eigenclass - Derived". Look for a singleton method. Found!
  • Call2: .. not found in eigenclass. But eigenclasses of class-objects have superclasses. Follow the golden arrow to Base's eigenclass. Found!
  • Call3: .. not found in Derived" or Base". But we still have a golden arrow to Object". Found!
  • Call4: .. not found in Derived"/Base"/Object".Try BasicObject" too. Not found. Now we're out of superclasses. Now we follow the purple arrow to the class-object of Derived i.e. Class. Look for instance methods defined there. Found!
  • Call5: .. not found in the eigenclass hierarchy or class-object. Try included modules in Class. not found. But wait, we have a superclass. Down the pink arrow to Module. Look for an instance method... found!
  • Call6: .. not found in eigenclass hierarchy/Class-n-included-modules/Module. Try included modules in Module. Nope. Down the pink arrow to Object. Found an instance method!
  • Call7: .. not found in eigenclass hierarchy/Class-n-incl-modules/Module-n-incl-modules/Object. Look in included modules of Object.. aha Kernel. Look for method.. found!



If you make one more call, you should now be guessing that a NoMethodError should show up from Kernel#method_missing(similar to the previous example)

So lets refactor the steps.
  1. Look for the method in the eigenclass (yellow arrow). Until we run out of superclasses for the eigenclass, inspect each eigenclass for the method.
  2. If not found, traverse the class attribute (purple arrow) to find the class-object.
  3. Look in the instance methods for this class
  4. If not found, try included modules of the class (in reverse order of mixin)
  5. If not found, check if we have a superclass (pink arrow). If yes, repeat steps 3-5 till we run out of superclasses
  6. If still not found, go back to Step1 but this time look for a 'method_missing' method

So the only new tidbits were
  • class methods are singleton methods of class-objects. They live in the green clouds tied with the yellow arrows to the class.
  • eigenclass of a class-object is special. These special 'green clouds' have superclasses.
And that is the secret behind class method inheritance... See the first three lines of the output. (Snuck another one.. {snicker} No more I promise.)

And that also explains how class method calls can result in instance methods being called. (Lines4-7)

And that's it folks! You will wake up enlightened or confused when I snap my fingers :)


As an exercise to people who like to stay and flex some meta-programming muscles. Try and explain this predicament.

Module defines a constants method, which returns the list of all known constants (incl. class names).
Module.constants => 102
Any user-defined class e.g. Base or Derived inherits this method ; however Base.constants returns only the constants defined inside Base (and its superclasses). e.g. If I add a constant inside Base.
Base.constants => 1
How does Module's constants behave differently in the Base (which inherits this functionality)?

Hint: Check Module.singleton_methods and Module.public instance_methods
Hint2: Hit Stackoverflow and search for the answer once you have taken a good crack at it.

1 comment:

  1. Didn't go over your blog post, and too tired to think very straight, but a couple of things:
    - best call it "singleton class" (there's even a singleton_class method coming up in 1.9.2)
    - always check the hierarchy of stuff in Ruby 1.9, because Ruby 1.8's doesn't always return something meaningful for superclass (in particular for singleton classes of classes)
    - The call order is complex. Hopefully I won't mess this up:
    1) singleton class
    2) class
    3) included modules (starting with the last included)
    4) superclass

    To complicate matters, the a_class.singleton_class.superclass == a_class.superclass.singleton_class!

    Of course, if the method is not found, then method_missing is called. It that isn't found, then an exception is raised.

    I'll improve this when I can think better... :-)

    ReplyDelete