skip to content

In Ruby, when a FileLog module's log method calls super, which method does that super reach, and what happens if nothing follows?

level: middleimportance: should knowfreq 42%

answer

  1. no fixed parent for a module
  2. next entry after the module
  3. decided by the receiver's chain
  4. super: no superclass method
  5. defined?(super) as a guard

basics

~20 s

super in a module method continues the search from the entry after that module in the receiver's ancestors, so its target depends on the host class. If no later entry defines the method, Ruby raises NoMethodError (super: no superclass method).

solid answer

~40 s

A module has no superclass of its own, so `super` inside `FileLog#log` means "continue the method search from the entry after `FileLog` in the ancestors of the object that received the call". Included into `Job`, that next entry is `Job`'s superclass chain; included into `ImportJob` alongside `ConsoleLog`, it may be `ConsoleLog#log`; prepended, it is the class's own `log`. The same module can therefore reach different methods in different hosts. If nothing further along defines `log`, Ruby raises `NoMethodError` with `super: no superclass method 'log'`. A module meant to be stackable calls `super` and each layer adds its part; a module that may be the last in the chain can check `defined?(super)` first.

code

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

class ImportJob
  include ConsoleLog
  include FileLog
end

class ExportJob
  include FileLog
end

ImportJob.new.log("row 7") # => "file(console: row 7)"
ExportJob.new.log("row 7") # NoMethodError: super: no superclass method 'log' ...

go deeper

for a junior

Know that super in a module method goes to whatever comes next in the host class's chain, and fails with NoMethodError when nothing does.

for a middle

Explain super as continuing the search after the current entry, show how one module reaches different methods in different hosts, and use defined?(super) as a guard.

for a senior

Design stackable modules that each call super, diagnose a stack where one layer forgot super or has no layer below, and explain why include order sets the layer order.

for a principal

Decide whether layered super chains are worth their implicit ordering compared with explicit composition, where each step is named and the order is visible in one place.

## super continues the search For a class, `super` looks like a call to the parent class's method. The precise rule, and the one that explains modules, is different: `super` **continues the method search** from the entry after the one where the current method was found, in the ancestors of the receiver's class. A module is not in any fixed hierarchy. Where it sits depends on which class included or prepended it, so the target of its `super` is decided by the host, not by the module. ## One module, three hosts ```ruby module FileLog def log(msg) = "file(#{super})" end ``` | Host | Chain around `FileLog` | `super` in `FileLog#log` reaches | |---|---|---| | `class Job < BaseJob; include FileLog` | `Job, FileLog, BaseJob` | `BaseJob#log`, or further up | | `class ImportJob; include ConsoleLog; include FileLog` | `ImportJob, FileLog, ConsoleLog` | `ConsoleLog#log` | | `class Report; prepend FileLog` | `FileLog, Report` | `Report#log` | The module's source is identical in all three; the answer changes because the chain changes. ## Stacking modules with super When every module in a stack calls `super`, each layer wraps the next, which is a common way to compose behaviour such as formatting, redaction and output: 1. `Redacted#log` masks secrets and calls `super`. 2. `Timestamped#log` adds a time and calls `super`. 3. `ConsoleLog#log` writes the line and does **not** call `super`, ending the chain. The order in which the modules are included decides the order of the layers. A layer that forgets `super` silently cuts off everything after it, which is the most common bug in such stacks. ## When nothing follows If `super` is called and no later entry in the chain defines the method, Ruby raises `NoMethodError` with the message `super: no superclass method 'log'` followed by a description of the receiver. That happens when: - the module is included in a class whose superclasses do not define `log`; - the module is the last layer of a stack that expected another layer below it; - a prepended wrapper calls `super` in a class that does not define the wrapped method. ## Guarding with defined?(super) `defined?(super)` returns `"super"` when a later entry defines the method and `nil` otherwise, without calling anything. A module that may or may not be the last layer can use it: ```ruby module Timestamped def log(msg) line = "#{Time.now.utc} #{msg}" defined?(super) ? super(line) : line end end ``` This keeps the module usable both on top of another logger and on its own. ## Reading a stack from the ancestors When a layered `log` produces unexpected output, the ancestors list tells you exactly what ran: 1. Print `obj.singleton_class.ancestors` to get the full list for that object. 2. Keep only the entries that define `log`; `ConsoleLog.instance_methods(false)` and similar calls show which do. 3. Start at the first one and follow each `super` to the next entry that defines `log`. 4. Stop at the first entry whose `log` does not call `super`; nothing after it runs. This turns a confusing output such as a missing timestamp into a concrete finding, for example "`ConsoleLog` is now included after `Timestamped`, so it ends the chain before the timestamp layer". ## Things that are not the target - `super` does not go to the module that included this one, or to the module's "parent"; modules have none. - `super` does not restart at the top of the chain; it only moves forward. - A method with the same name in the **singleton class** of the receiver has already been passed if the current method was found later, so `super` cannot reach back to it. - How the arguments are forwarded (bare `super` versus `super()` versus explicit arguments) is a separate rule from where the call goes.

  • Where does super go from a module extended onto ImportJob when it implements a class-level log?
    The same rule applies to the class-method chain, `ImportJob.singleton_class.ancestors`: the singleton class of `ImportJob`, the extended module, then the singleton class of `Job`. So `super` from the extended module's `log` reaches a `def self.log` defined in `Job`, if there is one.
  • What does defined?(super) return when a later ancestor defines the method?
    The string `"super"`; when no later entry defines it, `nil`. It evaluates nothing, so it is a safe guard before calling `super` in a module that may be the last layer of a stack.

super from a mixin is like a relay runner passing the baton to whoever is next in this particular race's line-up: the runner has no fixed teammate, so the same runner hands off to a different person in a different race, and the last runner has nobody to pass to.

saying these in an interview costs you the question

  • super in a module always calls Object's method of the same name
  • super in a module goes to the module that included it
  • A module's super target is fixed when the module is written
  • super with no later definition quietly returns nil
  • defined?(super) calls the next method to find out if it exists