Instantly share code, notes, and snippets.

Embed
What would you like to do?
A Primer on Ruby Method Lookup

A Primer on Ruby Method Lookup

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. If you are having trouble following method lookup in your own programs, it is not because Ruby has strange rules (it does), it is because your code is too tangled.

When you pass a message to an object, here is how Ruby finds what method to call:

1. Look within singleton class

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'

2. Look within modules that extend 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'

3. Look within methods defined on class

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'

4. Look within modules that were mixed in when class was defined

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'

Note:

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'

5. Climb the ancestors chain

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.

6. Start again, check method_missing

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'

Super

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"

One More Step

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

BONUS: Helper methods

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 the singleton

arr = Array.new
def arr.new_method; end

arr.methods(false)
# => [:new_method]

References:

@CaryInVictoria

This comment has been minimized.

CaryInVictoria commented Dec 16, 2018

This is very clear and useful. One small suggestion: where you have object.extend(MyModule) it might be helpful to point out that that is equivalent to object.singleton_class.include(MyModule).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment