skip to content

In Ruby, for ImportJob < Job that prepends Redacted and includes ConsoleLog then FileLog, what does ancestors return, and where do extended modules fit?

level: middleimportance: must knowfreq 48%

answer

  1. prepends before the class
  2. includes after it, last first
  3. then the superclass and its mixins
  4. singleton class heads one object's chain
  5. include A, B keeps A first

basics

~20 s

ImportJob.ancestors is [Redacted, ImportJob, FileLog, ConsoleLog, Job, ...]: prepended modules, the class, included modules with the most recent first, then the superclass chain. For one object, its singleton class and extended modules come before all of that.

solid answer

~40 s

Ruby builds one chain per class: **prepended** modules first (the last prepended at the front), then the **class**, then its **included** modules with the most recently included first, then the **superclass** with its own prepends and includes, down to `Object`, `Kernel` and `BasicObject`. So `prepend Redacted; include ConsoleLog; include FileLog` gives `[Redacted, ImportJob, FileLog, ConsoleLog, Job, Object, Kernel, BasicObject]`, and `job.log` runs `Redacted#log` if it defines one. `include ConsoleLog, FileLog` in one call keeps the listed order, `[ImportJob, ConsoleLog, FileLog, ...]`, because the arguments are processed in reverse. For a single object, its **singleton class** comes first, followed by modules the object was extended with, and only then the class's prepended modules.

code

ruby · 19 lines
ruby
module Redacted;   def log(msg) = "redacted(#{super})"; end
module ConsoleLog; def log(msg) = "console: #{msg}"; end
module FileLog;    def log(msg) = "file(#{super})"; end

class Job; end

class ImportJob < Job
  prepend Redacted
  include ConsoleLog
  include FileLog
end

ImportJob.ancestors.take(5) # => [Redacted, ImportJob, FileLog, ConsoleLog, Job]
ImportJob.new.log("row 7")  # => "redacted(file(console: row 7))"

class BatchJob < Job
  include ConsoleLog, FileLog # one call keeps the written order
end
BatchJob.ancestors.take(3)  # => [BatchJob, ConsoleLog, FileLog]

go deeper

for a junior

Remember the outline: prepends, the class, includes, then the superclass, and that the newest include is searched first.

for a middle

Build the ancestors list for a class with prepends, several includes and a superclass, and explain the one-call multi-argument exception.

for a senior

Use the full order, including the singleton class and extended modules, to explain a surprising method in production code and to decide where a new module must be attached to win.

for a principal

Weigh the readability cost of layered prepends and per-object extensions against their flexibility, and set conventions that keep the winning method easy to predict.

## The full search order For a method call on an object, Ruby searches, in this order: 1. The object's **singleton class** (methods defined on that one object), then modules the object was **extended** with, most recent first. 2. Modules **prepended** to the object's class, the most recently prepended first. 3. The **class** itself. 4. Modules **included** into the class, the most recently included first. 5. The **superclass**, with steps 2 to 4 repeated for it, and so on up to `Object`, `Kernel` and `BasicObject`. `Module#ancestors` on the class shows steps 2 to 5. `obj.singleton_class.ancestors` adds step 1 in front. ## A worked example ```ruby module Redacted; def log(msg) = "redacted(#{super})"; end module ConsoleLog; def log(msg) = "console: #{msg}"; end module FileLog; def log(msg) = "file(#{super})"; end module Timestamped; def log(msg) = "ts(#{super})"; end class Job include Timestamped def log(msg) = "job: #{msg}" end class ImportJob < Job prepend Redacted include ConsoleLog include FileLog end ImportJob.ancestors.take(6) # => [Redacted, ImportJob, FileLog, ConsoleLog, Job, Timestamped] ImportJob.new.log("row 7") # => "redacted(file(console: row 7))" ``` The call starts at `Redacted`, whose `super` reaches `ImportJob` (no `log` there), then `FileLog`, whose `super` reaches `ConsoleLog`, which returns without calling `super`, so `Job#log` and `Timestamped#log` never run. ## Last first, and the one-call exception Each `include` inserts the module **directly after the class**, pushing earlier includes further back. That is why separate calls produce "last included, first searched". The same holds for `prepend`: each new prepend goes to the very front of the chain, ahead of earlier prepends. A single call with several arguments is different. `include(A, B)` is documented as calling `append_features` on each argument **in reverse order**: `B` is inserted first, then `A` in front of it, so the final order matches the order you wrote. | Code in the class body | Resulting order after the class | |---|---| | `include ConsoleLog` then `include FileLog` | `FileLog, ConsoleLog` | | `include ConsoleLog, FileLog` | `ConsoleLog, FileLog` | | `prepend A` then `prepend B` | `B, A` before the class | | `prepend A, B` | `A, B` before the class | ## Where one object's modules go Methods defined on a single object, and modules it was extended with, live in that object's singleton class, which Ruby searches before anything in the class chain: ```ruby job = ImportJob.new job.extend(Audit) job.singleton_class.ancestors.take(4) # => [#<Class:#<ImportJob:0x...>>, Audit, Redacted, ImportJob] ``` So a module extended onto one object beats even a module prepended to its class. The exception is a module the class chain already contains, such as `Timestamped` through `Job`: extending with it adds nothing, because Ruby skips a module that is already anywhere further along the chain, so it cannot be moved in front that way. ## Common prediction mistakes - Putting included modules in the order they appear in the file. Separate `include` calls end up newest first. - Treating a prepend like an include with higher priority but still after the class. A prepended module is searched **before** the class's own methods. - Forgetting the superclass's modules. `Job`'s includes sit between `Job` and `Object`, not at the end of the list. - Assuming an extended module can be moved in front by extending again. If the module is already anywhere in the chain, extending or including it again changes nothing. - Mixing up the instance chain and the class-method chain. Class methods are searched through the singleton classes of the class and its superclasses. ## Predicting in an interview - Write the class in the middle, prepends to its left (newest leftmost), includes to its right (newest nearest the class). - Append the superclass and repeat the pattern for it. - For a specific object, put its singleton class and extended modules in front. - Then scan left to right for the first definition of the method; follow `super` calls to the right.

  • Where do modules prepended to Job appear in ImportJob.ancestors?
    Immediately before `Job`, after all of `ImportJob`'s own includes: `[..., ImportJob, FileLog, ConsoleLog, JobPrepends..., Job, JobIncludes..., Object, ...]`. Each class contributes its own block of prepends, itself and includes, and the blocks follow the superclass order.
  • Why does a method defined on one object with def job.log beat a prepended module's log?
    A method defined on a single object lives in its singleton class, and the singleton class is searched before the class chain, including the class's prepended modules. Only other entries in the singleton class's own chain, such as modules prepended to it, can come earlier.

saying these in an interview costs you the question

  • Separate include calls leave the first included module first in ancestors
  • Prepended modules come after the class in ancestors
  • include ConsoleLog, FileLog gives the same order as two separate includes
  • A module prepended to the class beats a module extended onto the object
  • The superclass is searched before the class's own included modules