skip to content

In Ruby, what does Module#ancestors return for a class, and how does Ruby use that list to pick which log method runs?

level: juniorimportance: must knowfreq 55%

answer

  1. one ordered list per class
  2. class, its mixins, then superclass
  3. first definition found wins
  4. ends Object, Kernel, BasicObject
  5. exhausted: method_missing, NoMethodError

basics

~20 s

Module#ancestors returns the ordered list of classes and modules Ruby searches for an instance method: the class, its mixed-in modules, then the superclass chain down to BasicObject. Calling job.log runs the first log found walking that list.

solid answer

~40 s

`ancestors` returns an Array of the class itself plus every module included or prepended into it and into its superclasses, in the order Ruby searches them. For `class Job; include FileLog; end`, a plain script gives `[Job, FileLog, Object, Kernel, BasicObject]`. When you call `job.log`, Ruby walks the object's class chain in exactly that order and runs the **first** `log` it finds, so a method defined in `Job` beats one in `FileLog`, which beats anything in `Object`. The list is live: including a module later changes it for all instances. If no entry defines the method, Ruby falls back to `method_missing`, whose default raises `NoMethodError`.

code

ruby · 16 lines
ruby
module FileLog
  def log(message) = "file: #{message}"
end

class Job
  include FileLog
end

class ImportJob < Job
  def log(message) = "import: #{message}"
end

ImportJob.ancestors.take(3)  # => [ImportJob, Job, FileLog]
ImportJob.new.log("started") # => "import: started"
Job.new.log("started")       # => "file: started"
Job.new.flush                # NoMethodError: nothing in the chain defines flush

go deeper

for a junior

Recall the shape of the list, the class, its modules, the superclass, then Object, Kernel and BasicObject, and that the first match wins.

for a middle

Explain that included modules sit right after their host, that the chain is live, and what happens when the chain is exhausted.

for a senior

Use ancestors to debug which method runs in a codebase with many mixins, and explain why a host's own method beats every included module.

for a principal

Judge how deep a mixin stack a team should allow: every added module lengthens the chain readers must hold in their heads to know which method runs.

## What ancestors is Every Ruby class and module answers `ancestors`, which the core documentation describes as the list of modules included or prepended in the receiver, **including the receiver itself**, together with its superclasses. It is the ordered list Ruby searches when it needs an instance method. ```ruby module FileLog def log(message) = "file: #{message}" end class Job include FileLog end class ImportJob < Job; end ImportJob.ancestors # => [ImportJob, Job, FileLog, Object, Kernel, BasicObject] ``` Reading the result left to right: - **`ImportJob`**: the class itself. - **`Job`**: its superclass. - **`FileLog`**: a module `Job` included; it sits right after `Job`, before `Job`'s superclass. - **`Object`**: the default superclass of every class written without `< Something`. - **`Kernel`**: a module `Object` includes; methods such as `puts` and `require` live there. - **`BasicObject`**: the root of the class hierarchy. Libraries can add modules to `Object` too. Once the `pp` library is loaded (the first call to `Kernel#pp` loads it), `Object.ancestors` also shows `PP::ObjectMixin`, so lists printed after other code has run can be longer than in a bare script. ## How a method call uses the list For a call such as `ImportJob.new.log("started")`, Ruby: 1. Starts at the object's class (after any per-object methods, which come first). 2. Checks each entry of the ancestors list in order for an instance method named `log`. 3. Runs the **first** one it finds and stops searching. 4. If none has it, calls `method_missing` on the object; the default implementation raises `NoMethodError`. There is no merging and no ambiguity error. Two definitions of `log` never clash; the one earlier in the list simply wins, and the later one is reachable only if the earlier one calls `super`. | Where `log` is defined | Result of `ImportJob.new.log` | |---|---| | only in `FileLog` | `FileLog#log` | | in `Job` and `FileLog` | `Job#log` (earlier in the list) | | in `ImportJob`, `Job` and `FileLog` | `ImportJob#log` | | nowhere | `NoMethodError` via `method_missing` | ## The list is live `ancestors` is not frozen when the class is defined. The lookup reads the current chain on every call: - including a module into `Job` later inserts it for every existing and future instance; - defining a new method in `FileLog` later makes it visible at once; - nothing can remove a module from the chain again, so the list only grows. Ruby caches method lookups for speed, and invalidates those caches whenever the chain or a method table changes, so the behaviour is always the current chain's. ## Class methods have a chain too `ancestors` describes the search for **instance** methods. A class method such as `ImportJob.run` is an instance method of the class object's singleton class, so its search follows a parallel chain: the singleton class of `ImportJob`, then that of `Job`, then that of `Object` and `BasicObject`, then `Class`, `Module`, `Object`, `Kernel` and `BasicObject`. You can print it with `ImportJob.singleton_class.ancestors`. Modules extended onto a class appear in that chain, which is why `extend` gives class methods. | Call | List Ruby walks | |---|---| | `job.log` | `job.singleton_class.ancestors` (usually just `ImportJob.ancestors` behind an empty singleton class) | | `ImportJob.run` | `ImportJob.singleton_class.ancestors` | | a module method called through a host | the host's chain; `FileLog.ancestors` alone is just `[FileLog]` plus modules `FileLog` itself includes or prepends | ## Why interviewers ask for it Predicting `ancestors` is the quickest way to answer "which method runs?" in a codebase with several mixins. Two habits help: - When a method behaves unexpectedly, print `obj.class.ancestors` and look for every entry that defines the method; `instance_method(:log).owner` names the winner. - When designing a mixin, remember that a method the host class defines itself always wins over an included module's, so an included module provides defaults rather than overrides.

  • Does ancestors include the singleton class or modules extended onto one object?
    No. `ImportJob.ancestors` describes the class's chain. Methods defined on one object, and modules that object was extended with, live in its singleton class, which is searched before the class; `obj.singleton_class.ancestors` shows that longer list.
  • If Job includes FileLog after some ImportJob instances already exist, do those instances see FileLog#log?
    Yes. Instances do not hold a copy of the chain; every call walks the class's current ancestors. Including the module inserts it into the chain once, and every existing and future instance sees it on the next call.

saying these in an interview costs you the question

  • An included module comes after Object in the ancestors list
  • Ruby raises an ambiguity error when two ancestors define the same method
  • ancestors is fixed when the class is first defined
  • The last definition found in the list is the one that runs
  • Kernel is the superclass of Object