Ruby: Where Do Methods Actually Live?

Ruby: Where Do Methods Actually Live

You often hear that a method belongs to an object — but in Ruby, things are a bit more interesting.

Typically, methods are defined in classes and modules, and when you call one, Ruby searches for it along the ancestor chain. An individual object can also have its own methods — singleton methods, defined in its singleton class (eigenclass).

Where are methods located?

Let's look at a simple example:

ruby
class Post
  def title
    "Hello Ruby"
  end

  def self.recent
    []
  end
end

post = Post.new

def post.preview
  "Preview"
end

Here we have three different methods:

  • title — a regular instance method, defined in Post.
  • recent — a class method, defined in the singleton class of the Post object.
  • preview — a singleton method of the specific post instance.

The illustration shows exactly where these methods live. Note that Post is simultaneously a class for its instances and a regular object — an instance of Class.

That's precisely why Post itself also has a singleton class, in which its class methods are defined.

Post singleton class

How does Ruby look up methods?

Now let's make the example more complex:

ruby
module Logging
  def title = "Logging"
end

module Formatting
  def title = "Formatting"
end

class BasePost
  def title = "BasePost"

  def self.recent = []
end

class Post < BasePost
  prepend Logging
  include Formatting

  def title = "Post"
end

post = Post.new

def post.preview = "Preview"

When we call post.title, Ruby searches for the method in a specific order.

The lookup chain:

Look up chain

Ruby goes from top to bottom and stops at the first matching method.

Therefore:

ruby
post.title
# => "Logging"

post.preview
# => "Preview"

Even though title is defined in several places, the implementation from Logging wins because the module is included via prepend.

And preview is found directly in the singleton class of the post object.

You can check the chain yourself:

ruby
post.singleton_class.ancestors
# => [
#   #<Class:#<Post:...>>,
#   Logging,
#   Post,
#   Formatting,
#   BasePost,
#   Object,
#   Kernel,
#   BasicObject
# ]

An important nuance: prepend can also be used on a singleton class. In that case, modules included this way will be called before the class's own methods.

ruby
module Formatting
  def preview
    "Formatted Preview"
  end
end

class Post
end

def post.preview = "Preview"

post.singleton_class.prepend Formatting

post.preview

What about class methods?

Let's return to the illustration: it shows that class methods are not a separate Ruby entity. They are ordinary singleton methods of the class object.

For example, BasePost.recent is defined on the BasePost object, which itself is an instance of Class.

At the same time, class methods are inherited:

ruby
Post.recent
# => []

Why? Because the lookup follows a different chain:

Different lookup chain

A child's singleton class inherits from the parent's singleton class. This is why class methods can also be inherited.

This is exactly the distinction the illustration shows: instance methods live in the regular class, while the class's own methods live in its eigenclass.

And if a method isn't found?

When no matching method is found, Ruby falls back to method_missing. Notably, method_missing itself is also found through the method lookup mechanism.

If we implement dynamic methods via method_missing, we should usually also override respond_to_missing? so that respond_to? correctly reflects the supported methods.

Part of metaprogramming in Ruby and Rails is built on these mechanisms.

Example: passing unhandled calls up the inheritance chain

ruby
class BasePost
  def method_missing(name, *args, **kwargs, &block)
    if name == :hello
      "Hello from BasePost!"
    else
      super
    end
  end

  def respond_to_missing?(name, include_private = false)
    name == :hello || super
  end
end

class Post < BasePost
  def method_missing(name, *args, **kwargs, &block)
    if name == :goodbye
      "Goodbye from Post!"
    else
      super
    end
  end

  def respond_to_missing?(name, include_private = false)
    name == :goodbye || super
  end
end

post = Post.new

post.goodbye # => "Goodbye from Post!"
post.hello   # => "Hello from BasePost!"

post.unknown # => NoMethodError

post.respond_to?(:goodbye) # => true
post.respond_to?(:hello)   # => true
post.respond_to?(:unknown) # => false

The order of processing for post.hello:

  1. Ruby doesn't find a regular hello method and calls Post#method_missing.
  2. Post doesn't handle hello and calls super.
  3. Ruby moves on to BasePost#method_missing.
  4. BasePost handles hello and returns the result.

For post.unknown, the same thing happens, but BasePost#method_missing also calls super. Following the inheritance chain, Ruby reaches BasicObject#method_missing, which raises NoMethodError.

Important: super here calls the next implementation of method_missing specifically.

Refinements: beyond the ancestor chain

The chains shown above describe the normal method lookup. Active refinements can additionally influence which implementation is chosen.

Refinements allow you to locally extend or override methods of existing classes without changing their global behavior.

ruby
class Post
  def hello
    "Hello from Post"
  end
end

module PostRefinements
  refine Post do
    def hello
      "Hello from refinement"
    end

    def goodbye
      "Goodbye from refinement"
    end
  end
end

class ReplyPost < Post
end

module Example
  using PostRefinements

  post = Post.new
  reply = ReplyPost.new

  post.hello   # => "Hello from refinement"
  post.goodbye # => "Goodbye from refinement"
  reply.hello   # => "Hello from refinement"
  reply.goodbye # => "Goodbye from refinement"
end

Post.new.hello   # => "Hello from Post"
Post.new.goodbye # => NoMethodError

Post.ancestors
# => [Post, Object, Kernel, BasicObject]

What's happening here?

  1. Post defines a regular hello method.
  2. PostRefinements contains two alternative implementations: an overridden hello and a new goodbye.
  3. Inside the lexical scope where using PostRefinements is activated, Ruby takes the refinement into account when looking up methods for Post and its subclass instances.
  4. Outside that scope, the refinement methods are not accessible via a normal call.
  5. Notably, PostRefinements does not appear in Post.ancestors: a refinement does not attach a module to the class the way include or prepend do.

Thus, a method doesn't necessarily need a regular position in the ancestors chain for Ruby to be able to call it. Refinements add another factor to method resolution — the lexical context in which the calling code is defined.

Conclusion

In Ruby, methods are not stored separately in each ordinary object. They are defined in classes, modules, and singleton classes.

And here it's important to separate two questions:

  • Where is a method defined? — the illustration shows this.
  • How does Ruby find the right method? — the lookup chains explain this.

It is precisely the combination of these mechanisms that makes Ruby's object model so flexible.