Ruby Method Lookup Demystified: Inheritance, Mixins, and Super

Ruby Method Lookup Demystified: Inheritance, Mixins, and Super

Discover how method lookup works in Ruby, including inheritance, mixins using include, prepend, and extend, and the super method.

If you’ve worked on Ruby projects for a while, you would have encountered times when you were digging to find out how and why a certain method behaved the way it did, because it didn’t seem to use the code in the method definition you’re looking at. And what happens when there are multiple methods with the same name?

This can seem confusing, but Ruby does have a specific way or path that is used to determine what methods to actually call. This process is called “method lookup”. In this guide we will delve into the details of method lookup, covering inheritance, mixins using include, prepend, and extend, as well as the super method. With clear examples and explanations, this article will help you build a strong foundation and understanding that enables you to write more maintainable code.

Inheritance and Method Lookup in Ruby

In Ruby, inheritance allows a class to inherit the methods and attributes of another class. The class that inherits is called a subclass, and the class that is inherited from is called a superclass. Method lookup in Ruby begins by searching the instance methods of the object's class, and if not found, it moves up the class hierarchy until it reaches the superclass. Let's consider an example:

class Animal
  def speak
    "I'm an animal!"
  end
end

class Dog < Animal
end

dog = Dog.new
puts dog.speak # Output: I'm an animal!

In the example above, the Dog class inherits from the Animal class. When we call the speak method on a Dog object, Ruby first looks for the method in the Dog class. Since it doesn't find it there, it searches the superclass Animal, finds the speak method, and executes it.

Mixins and Method Lookup

Ruby uses modules as a way to implement multiple inheritance through mixins. Mixins are used to share methods among different classes, making your code more modular and easier to maintain. There are three primary ways to include a module in your class: include, prepend, and extend.

Include

include adds the module's methods as instance methods in the class. When a method is called on an object, Ruby first searches the object's class, then the included module, and finally the superclass.

module Speak
  def speak
    "I'm a speaking mixin!"
  end
end

class Dog < Animal
  include Speak
end

dog = Dog.new
puts dog.speak # Output: I'm a speaking mixin!

In this example, we've included the Speak module in the Dog class. When we call the speak method on a Dog object, Ruby first looks for the method in the Dog class, then in the included Speak module, and finally in the superclass Animal. Since it finds the speak method in the Speak module, it executes that method.

Prepend

prepend is similar to include, but it inserts the module's methods before the class in the method lookup chain.

class Dog2 < Animal
  prepend Speak
end

dog = Dog2.new
puts dog.speak # Output: I'm a speaking mixin!

In this case, when we call the speak method on a Dog object, Ruby first looks in the prepended Speak module, then in the Dog class, and finally in the superclass Animal. Since it finds the speak method in the Speak module, it executes that method.

Extend

extend adds the module's methods as class methods, rather than instance methods. This means that the methods can be called directly on the class itself.

class Dog < Animal
  extend Speak
end

puts Dog.speak # Output: I'm a speaking mixin

In this example, by using extend instead of include or prepend, we've added the speak method as a class method of Dog. This allows us to call the speak method directly on the Dog class.

The super Method

The super method in Ruby allows a subclass to call a method from its superclass. This can be useful when you want to extend the behaviour of a superclass method in a subclass without completely overriding it. Let's see an example:

class Animal
  def speak
    "I'm an animal!"
  end
end

class Dog < Animal
  def speak
    "#{super} and I'm a dog!"
  end
end

dog = Dog.new
puts dog.speak # Output: I'm an animal! and I'm a dog!

In the Dog class, we've defined a speak method that calls the speak method from its superclass Animal using the super keyword. This allows us to extend the behaviour of the Animal#speak method in the Dog class.

The super Method with Mixins

The super methods can still be used when mixins are involved, and for the purposes of this post is a great showcase of the method lookup flow.

In this example, we'll demonstrate the use of super in an object that uses both prepend and include to mixin the same method. We'll have three modules with a greet method, and a Person class that will include and prepend those modules.

module GreetInEnglish
  def greet
    "Hello! #{super}"
  end
end

module GreetInSpanish
  def greet
    "¡Hola! #{super}"
  end
end

module GreetInFrench
  def greet
    "Bonjour! #{super}"
  end
end

class LivingBeing
  prepend GreetInEnglish

  def greet
    "I'm a living being."
  end
end

class Person < LivingBeing
  include GreetInFrench
  prepend GreetInSpanish

  def greet
    "I'm a person. #{super}"
  end
end

person = Person.new
puts person.greet

In this example, we've added a LivingBeing class that includes GreetInEnglish and has its own greet method. The Person class now inherits from LivingBeing, includes GreetInFrench, and prepends GreetInSpanish.

When we call person.greet, the method lookup order is as follows:

  1. GreetInSpanish#greet (prepended in Person)

  2. Person#greet (defined in the class)

  3. GreetInFrench#greet (included in Person)

  4. GreetInEnglish#greet (prepended in LivingBeing)

  5. LivingBeing#greet (inherited from the superclass)

The output will be:

¡Hola! I'm a person. Bonjour! Hello! I'm a living being.

Now, the method lookup chain correctly includes all mixins and the inherited method from LivingBeing. The super keyword is used in each of the mixin methods and the Person#greet method to call the next method in the lookup chain.

Conclusion

Understanding Ruby's method lookup process helps you write more maintainable code and tackle complex Ruby projects and libraries. Use inheritance, mixins, and the super method to override and extend capabilities in Ruby when needed. Be mindful of code complexity and avoid hiding implementation details in too many places. Happy coding!

🧑🏾‍🚀🚀

Did you find this article valuable?

Support Building & Sharing with Unathi by becoming a sponsor. Any amount is appreciated!