The object model and method lookup

ancestors

ancestors is the method lookup order, printed as a list. The first entry is the class itself, then the modules it includes, then its superclass and so on:

ruby -e 'p String.ancestors.first(5)'

[String, Comparable, Object, Kernel, BasicObject]

A call on a string is looked up in String, then Comparable, then Object. That is why < works on a string: it comes from Comparable, which uses the <=> that String defines.

prepend

include puts a module after the class; prepend puts it before, so the module can wrap the class's own method and call super into it:

ruby -e 'module M; def hi = "M"; end; class A; def hi = "A"; prepend M; end; p A.ancestors.first(3); p A.new.hi'

[M, A, Object] "M"

The module wins because it is first in the list. A prepended module that calls super gets the class's version, which is the mechanism behind a decorator that does not replace the method.

Singleton classes

A method defined on one object lives in that object's singleton class, which sits in front of its real class for that one object only:

ruby -e 'class A; def hi = "A"; end; o = A.new; def o.hi = "single"; p o.hi; p A.new.hi'

"single" "A"

o.singleton_class shows the class, and o.singleton_class.ancestors puts it before A. A class method is the same idea one level up: A is an object, and def self.x defines a method on its singleton class.

The lookup path

A call on an object goes through these steps, and knowing the order answers most "why did this method run?" questions:

  1. The object's singleton class, if it has methods.
  2. The class itself.
  3. Each prepended module, in reverse order of prepending.
  4. Each included module, in reverse order of inclusion.
  5. The superclass chain, each with its own prepends and includes.
  6. method_missing, if nothing matched.

obj.method(:hi).owner answers which class or module supplied the method, which is faster than reading the list when a framework has added three layers.

What to watch for

Adding a module with include twice is harmless; prepending twice puts the module in the chain twice, and super calls it twice. Guard a prepend with a check or make the module idempotent.

super without arguments forwards the same arguments; super() passes none. The difference is the first thing to check when a wrapper changes a method's behaviour.

Next: method_missing done right

Article author: Arthur Isaev

Banner image