Method lookup is a simple affair in most languages without multiple inheritance. You start from the receiver and move up the ancestors chain until you locate the method. Because Ruby allows you to mix in modules and extend singleton classes at runtime, this is an entirely different affair.
I will not build contrived code to exemplify the more complicated aspects of Ruby method lookup, as this will only serve to confuse the matter.
When you pass a message to an object, here is how Ruby finds what method to call:
Simply put, a singleton method is a method that is defined on an instance as oppose to a class where the method would be available on all instances.
This takes precedence even over methods defined on the class.
Here is an example to demonstrate:
class MyCar
def method; 'defined on car'; end
end
object = MyCar.new
def object.method
'defined on singleton'
end
object.method # => 'defined on singleton'
If we can't find the method on the singleton, we'll take a look in any modules that extend the singleton.
You can extend singletons like so:
module MyModule
def method; 'defined on MyModule'; end
end
object = MyCar.new
object.extend(MyModule)
object.method # => 'defined on MyModule'
If you extend with multiple modules, later modules take precedence:
module MyModule2
def method; 'defined on MyModule2'; end
end
object.extend(MyModule)
object.extend(MyModule2)
object.method # => 'defined on MyModule2'
Considering you rarely extend singletons, this is usually where you would start in practice:
class MyCar
def method; 'defined on MyCar'; end
end
MyCar.new.method # => 'defined on MyCar'
So, the method doesn't exist within the class, but we mixed in a bunch of methods from a few modules:
module A
def method; 'defined on A'; end
end
module B
def method; 'defined on B'; end
end
class MyCar
include A
include B
end
Similar to when we extend a singleton, the later modules take precedence:
MyCar.new.method # => 'defined on B'
Ruby 2.0 introduced a prepend
method to accompany include
. Methods defined in a prepended module take precedence over methods defined on the class.
module A
def method; 'defined on A'; end
end
class MyCar
prepend A
def method; 'defined on MyCar'; end
end
# with include A
MyCar.new.method # => 'defined on MyCar'
# with prepend A
MyCar.new.method # => 'defined on A'
Ok, so we can't find the method on the singleton, we can't find it within the class definition, and we can't find it within any mixed in modules. What now? Well, we move up the ancestors chain and start from (3). Why not (1) you might ask? That's because the first two lookups involve singletons, whereas your ancestors are just modules/classes, not instances. This means we start with methods on the superclass, then lookup modules mixed into that class, and on we go to the next superclass.
We've reached the end of the ancestors chain and still can't find our method. What now? Ruby will now return back to (1) and run method_missing
. To give an example:
class BasicObject
def method; 'defined on Object'; end
end
class MyCar
def method_missing(name, *args, &block)
"called #{name}"
end
end
MyCar.ancestors # => [MyCar, Object, Kernel, BasicObject]
MyCar.new.method # => 'defined on Object'
The super
method looks up the ancestors chain for the method name where super
is called. It accepts optional arguments which are then passed onto that method. This will follow the same lookup path as outlined above from when the super
method was called.
class Vehicle
def method; 'defined on Vehicle'; end
end
class MyCar < Vehicle
def method
"defined on MyCar\n"
super
end
end
MyCar.new.method
# => "defined on MyCar"
# => "defined on Vehicle"
There is a rule that is used to think about the above behaviour called "one step to the right, and up", which can be described as: go right into the receiver's class (singleton), then up the ancestors chain. Here is a simple visualisation to further clarify:
Object
^
|
Parent class A
^
|
Module included by Parent Class B
^
|
Parent class B
^
|
object's class
^
|
obj -> obj's singelton_class
Here are a few methods to help you inspect the above in your own applications:
Array.ancestors
# => [Array, Enumerable, Object, Kernel, BasicObject]
# note: below returns Object because Enumerable is mixed in module (not a superclass)
Array.superclass
# => Object
# This will return all instance methods from the class, modules, superclasses
# (i.e. anything available to the class)
Array.instance_methods
# => [:huge, :list, :of, :methods]
# `false` argument below will only return methods defined within the Array class
Array.instance_methods(false)
# The following shows where the instance method is defined (Enumerable)
Array.instance_method(:reduce)
# => #<UnboundMethod: Array(Enumerable)#reduce>
# `false` argument below returns methods defined only on the singleton
arr = Array.new
def arr.new_method; end
arr.methods(false)
# => [:new_method]
- https://practicingruby.com/articles/method-lookup-1
- https://practicingruby.com/articles/method-lookup-2
- http://tech.pro/tutorial/1149/understanding-method-lookup-in-ruby-20
- http://stackoverflow.com/questions/23848667/ruby-method-lookup-path-for-an-object
- http://ruby-metaprogramming.rubylearning.com/html/ruby_metaprogramming_2.html
Great writeup, it helped me a lot today. I saw some lazy initialization code in gtk3 gem today and now I understand the trick much better. I had a minor problem with 6,
method_missing
was supposed to be called from the eigenclass, yet you define yours on the class itself as instance method. But I see your point now - that was the reason why Object caught it. Great resource for studying the meta-class too.