DEV Community

Cover image for In Ruby, Objects Don't Have Methods. How Method Lookup Actually Works.
S M Tahosin
S M Tahosin

Posted on

In Ruby, Objects Don't Have Methods. How Method Lookup Actually Works.

Ask almost any developer how object-oriented programming works, and they will describe the same textbook picture: an object bundles data and methods together into a single unit.

In Ruby, you hear this idea reinforced constantly under the slogan: "Everything is an object."

You create a string, call .upcase on it, and it feels completely natural to assume that the string object owns that method. You create a user object, call .save, and assume .save lives inside your user.

Except that is not how Ruby works at all.

In Ruby, objects contain zero methods.

If you inspect an instance down at the C source code level in CRuby (MRI), you discover something surprising: an object stores its instance variables, a set of internal flags, and a pointer to another structure. It does not contain a single line of method code, nor does it contain a method table.

Where do methods actually live? How does Ruby know what to execute when you type user.greet? And what actually happens when you define a method on a single specific object?

To truly master Ruby, you have to look beneath the syntactic sugar and understand the mechanics of Ruby's object model: method tables, the hidden eigenclass, and the ancestry ladder.


The First Surprise: Objects Store State, Classes Store Methods

Let us look at a simple Ruby class:

class User
  def initialize(name, email)
    @name = name
    @email = email
  end

  def greet
    "Hello, I am #{@name}"
  end
end

user = User.new("Alice", "alice@example.com")
Enter fullscreen mode Exit fullscreen mode

When Ruby evaluates User.new, the virtual machine allocates a heap structure called an RObject.

Inside this RObject, Ruby stores two things:

  1. An array of instance variables: @name set to "Alice", and @email set to "alice@example.com".
  2. An internal pointer named klass, pointing directly to the User class object.

Notice what is missing from user: the greet method is nowhere to be found.

Ruby Object Memory: Heap Object RObject vs Class Object RClass

Methods in Ruby live exclusively inside classes (represented internally as RClass).

The User class contains a hash table called m_tbl (the method table). When you define def greet, Ruby writes an entry into User's method table mapping the symbol :greet to its compiled bytecode body.

When you call user.greet:

  1. Ruby does not look inside user.
  2. Ruby follows user's internal klass pointer to the User class.
  3. Ruby searches User's m_tbl for :greet.
  4. It finds the method and executes it in the context of self = user.

This separation is what makes Ruby memory-efficient. You can instantiate one hundred thousand User objects, and all one hundred thousand share the exact same method table inside User. The instances only hold their personal instance variables.


The Mystery of Singleton Methods

Now consider this famous Ruby feature: you can add a method to just one individual object without affecting any other instance of that class.

str1 = "hello"
str2 = "world"

def str1.shout
  upcase + "!!!"
end

str1.shout #=> "HELLO!!!"
str2.shout #=> NoMethodError: undefined method 'shout' for "world":String
Enter fullscreen mode Exit fullscreen mode

This raises a fascinating puzzle:

  • We already know that objects cannot hold methods. So :shout cannot live inside str1.
  • :shout cannot live inside String, because then str2 would also have access to it.

If objects cannot hold methods, and the class cannot hold this method, where on earth does shout live?

Ruby solves this by doing something brilliant: it quietly invents an invisible class on the fly.

Defining a Singleton Method: The Hidden Eigenclass

The moment you define a method on str1, Ruby creates an anonymous, hidden class called an eigenclass (officially referred to as a singleton class).

Ruby then performs an architectural sleight of hand:

  1. It intercepts str1's internal klass pointer and repoints it to this new singleton class (#<Class:str1>).
  2. It sets the superclass of the singleton class to String.
  3. It places the :shout method inside the singleton class's method table.

When you call str1.shout, Ruby follows the klass pointer into #<Class:str1>, finds :shout, and runs it.

When you call str1.upcase, Ruby looks in #<Class:str1>, does not find :upcase, follows the superclass pointer up to String, finds :upcase, and runs it.

To regular code, the singleton class is almost completely transparent. You do not see it in str1.class (which still reports String). But you can prove it exists with Ruby's reflection methods:

str1.singleton_class
#=> #<Class:#<String:0x00007fffa80c2180>>

str1.singleton_methods
#=> [:shout]
Enter fullscreen mode Exit fullscreen mode

"Class Methods" Do Not Exist

Once you understand eigenclasses, one of the biggest myths in Ruby evaporates: there is no such thing as a "class method".

Developers frequently write code like this:

class Post
  def self.recent
    # query recent posts
  end
end
Enter fullscreen mode Exit fullscreen mode

Tutorials often say: "This defines a class method on Post."

In languages like Java or C++, a static method is a distinct language construct. But in Ruby, Post is not a special entity. Post is simply an object. It is an instance of the class Class.

Post.class
#=> Class
Enter fullscreen mode Exit fullscreen mode

Because Post is an object, writing def Post.recent or def self.recent is doing the exact same thing as writing def str1.shout: it is defining a singleton method on an object.

Class Methods Are Just Singleton Methods on Class Objects

The :recent method does not live in Post, and it does not live in Class. It lives in Post's own private eigenclass (#<Class:Post>).

In Ruby, there are only instance methods. What we call "class methods" are merely instance methods living on the singleton class of a class object. The entire language operates on one single, unified rule.


The Complete Method Lookup Ladder

When you type receiver.message, what is the exact order Ruby follows to find the code?

Ruby does not search haphazardly. It walks an exact, deterministic ladder called the ancestors chain. The first match it encounters executes immediately.

The Ruby Method Lookup Ladder

Here is the exact step-by-step lookup path:

  1. The Receiver's Eigenclass: If the object has any singleton methods, they get first priority.
  2. Prepended Modules on the Eigenclass: Any module added with prepend sits in front of the eigenclass.
  3. The Receiver's Class: The standard class where ordinary instance methods reside.
  4. Prepended Modules on the Class: If a module was prepended to the class, its methods override the class's own methods.
  5. Included Modules on the Class: If a module was included in the class, it is inserted directly above the class in the hierarchy.
  6. Superclasses (and their modules): Ruby climbs to the parent class, checking its prepends, own methods, and includes.
  7. Object, Kernel, and BasicObject: The root ancestors shared by virtually all Ruby objects.

What Happens When Nothing Matches?

If Ruby climbs all the way to BasicObject without finding a matching method, it does not immediately raise an error.

Instead, Ruby walks back down to the starting receiver and starts the entire search again from step one, this time searching for a method named method_missing.

Because Kernel provides a default implementation of method_missing that raises NoMethodError, your program only crashes if no intermediate module or class intercepts it.

This two-pass search is what powers Ruby's famous metaprogramming libraries, from ActiveRecord dynamic finders to OpenStruct.


How "Everything Is an Object" Runs Fast: Tagged Pointers

If every number, boolean, and nil value in Ruby is an object with methods, how does Ruby avoid grinding to a halt allocating heap memory for basic math?

If you run:

total = 0
1_000_000.times { |i| total += i }
Enter fullscreen mode Exit fullscreen mode

Does Ruby allocate one million RObject structs on the heap?

In CRuby, the answer is no, thanks to an internal optimization called Tagged Pointers (Immediate Values).

CRuby Tagged Pointers: Immediate Values

On a 64-bit operating system, memory addresses are aligned to 8-byte boundaries. That means the lowest 3 bits of any valid heap pointer are always 000.

CRuby takes advantage of these unused bits. When a 64-bit value represents a small integer (called a Fixnum), Ruby shifts the integer left by one bit and sets the lowest bit to 1:

value = (integer << 1) | 1
Enter fullscreen mode Exit fullscreen mode

When the Ruby virtual machine sees a value with the lowest bit set to 1, it knows: "This is not a heap pointer. This is the raw integer itself."

Similarly, CRuby reserves specific bit patterns for singleton values:

  • false is represented as 0x00
  • nil is represented as 0x08
  • true is represented as 0x14

When you call 42.even?, Ruby does not inspect a heap block. It detects the tagged pointer, looks up the method in the Integer class method table, and executes the code without a single byte of memory allocation or garbage collection overhead.

You get the clean conceptual purity of "everything is an object" combined with the efficiency of primitive register arithmetic.


The Mental Shift

Ruby's object model is often described as complex, but under the surface, it is remarkably minimal:

  1. Objects store state and an internal klass pointer. They never hold methods.
  2. Classes store methods and an internal super pointer. They never hold instance state.
  3. Singleton methods live in invisible eigenclasses, seamlessly spliced into the lookup chain.
  4. Class methods are just singleton methods on class objects, because classes are objects too.
  5. Tagged pointers preserve pure object semantics without sacrificing performance.

Once you see the object model through this lens, Ruby metaprogramming stops feeling like black magic. Concepts like class_eval, instance_eval, prepend, and singleton classes become predictable mechanical tools that follow one coherent set of rules.

When you first learned Ruby, what was the quirk in its object model that made you stop and rethink how programming languages work? And if you have used eigenclasses in production, what was the problem you solved with them?


References:

Top comments (1)

Collapse
 
kreout profile image
Andrei Ovcharenko •

The lookup ladder has two entries reversed: a prepended module is searched before the class it was prepended to. That also applies when the target is a singleton class.

A small example makes the ordering visible:

module Wrapper
  def greet
    "wrapped #{super}"
  end
end

class User
  def greet
    "hello"
  end
  prepend Wrapper
end

User.ancestors.take(2) # => [Wrapper, User]
User.new.greet        # => "wrapped hello"
Enter fullscreen mode Exit fullscreen mode

So steps 1/2 and 3/4 need swapping. Otherwise the listed order suggests the original method wins, which is exactly the behavior prepend changes. The official Ruby FAQ shows the same ordering with String#capitalize.