skip to content

In Ruby, how can a `logged` class-level call use method_added to wrap the next method defined, and why can a naive version raise SystemStackError?

level: seniorimportance: should knowfreq 20%

answer

  1. flag now, wrap in the hook
  2. method_added runs after the def
  3. define_method fires method_added
  4. clear the flag before redefining
  5. or prepend a Module.new wrapper

basics

~20 s

logged sets a flag, and method_added(name) wraps the method just defined. If the wrapper is defined with define_method in the same class before the flag is cleared, that definition fires method_added again, endlessly. Clear the flag first, or prepend the wrapper.

solid answer

~40 s

Ruby calls `method_added(name)` after every instance method definition in a class, with the new method already in place. A decorator-style macro exploits that: `logged` sets `@log_next = true`, and the class's `method_added` checks the flag, grabs the original with `instance_method(name)`, and redefines `name` with `define_method` to log and call it. The trap: `define_method` is itself a method definition in the same class, so it fires `method_added(name)` again; if the flag is still set, the hook wraps the wrapper, which fires the hook again, until `SystemStackError`. Two fixes: clear the flag **before** redefining, or define the wrapper in a `Module.new` and `prepend` it — definitions inside that module call the module's own `method_added`, not the class's, and the wrapper reaches the original with `super`. Always call `super` in the hook so other hooks still run.

code

ruby · 28 lines
ruby
module Logged
  def logged
    @log_next = true
  end

  def method_added(name)
    super
    return unless @log_next
    @log_next = false
    prepend(Module.new do
      define_method(name) do |*args, &block|
        puts "-> #{name}"
        super(*args, &block)
      end
    end)
  end
end

class Report
  extend Logged

  logged
  def render = :ok
  def other = :plain
end

Report.new.render # prints "-> render", returns :ok
Report.new.other  # => :plain, not wrapped

go deeper

for a junior

Recall that method_added(name) is called on a class after an instance method is defined in it.

for a middle

Explain the flag-then-wrap pattern and why the hook sees the new method through instance_method.

for a senior

Trace the recursion step by step and choose between clearing the flag first and a prepended wrapper module.

for a principal

Judge whether hidden decorators are clearer than explicit wrapping for a shared codebase, and set rules for hook use.

## The pattern: decorate the next method Some languages have syntax for method decorators. Ruby builds the same thing from a hook. The user writes: ```ruby class Report extend Logged logged def render = :ok end ``` `logged` is a class-level method (supplied by the extended module). It cannot see the next method yet, because that `def` has not run. So it sets a flag, and the class's **`method_added`** hook does the wrapping when the next definition arrives. ## Timing of method_added - Ruby calls `method_added(name)` on the class **after** the method is stored, so `instance_method(name)` inside the hook returns the new method. - It fires for every instance-method definition in that class: `def`, `define_method`, `attr_*`, `alias_method`. - It does **not** fire for `def self.x`; singleton methods go through `singleton_method_added`. - It runs on every definition, so the hook itself must be cheap and must ignore names it does not care about. ## The naive version and its recursion ```ruby module Logged def logged @log_next = true end def method_added(name) super return unless @log_next original = instance_method(name) define_method(name) { |*args| puts "-> #{name}"; original.bind(self).call(*args) } @log_next = false # too late end end ``` What happens when `def render` runs: 1. `method_added(:render)` sees the flag and captures the original `render`. 2. `define_method(:render)` stores the wrapper in `Report` — another definition. 3. Ruby calls `method_added(:render)` again. The flag is still true, so it captures the wrapper and defines another wrapper. 4. Step 3 repeats until the stack overflows: `SystemStackError: stack level too deep`. ## Two fixes | Fix | How | Why it works | |---|---|---| | Clear the flag first | set `@log_next = false` before `define_method` | the nested `method_added` call returns immediately | | Prepend a wrapper module | `prepend(Module.new { define_method(name) { \|*a\| super(*a) } })` | the definition happens in the anonymous module, so the class's `method_added` is not called; `super` reaches the original | The prepend version has further advantages: the original method stays untouched in the class, several decorators stack in the order they were prepended, and the wrapper module appears in `Report.ancestors`. ## Where the flag lives Both `logged` and `method_added` run with `self` set to the class (`Report`), because the module was added with `extend`. So `@log_next` is an instance variable of the class object itself: - each class that extends `Logged` has its own flag, so decorating in one class cannot leak into another; - a subclass starts with no flag set, even if its parent's flag was left on by mistake; - the flag must be reset in every path, including when the hook decides not to wrap, or the next unrelated method gets decorated. ## Related hooks - **`method_removed`** and **`method_undefined`** keep a registry of decorated methods accurate when methods are later removed or undefined. - **`singleton_method_added`** is the hook to use if the decorator should also apply to `def self.x` methods; note that defining that hook with `def self.singleton_method_added` reports its own name immediately. ## Production checklist 1. Call `super` in every hook; another library may be using the same hook on the same class. 2. Reset the flag before any definition the hook performs. 3. Keep the hook fast: it runs for every method the class defines, including generated accessors. 4. Test that decorating two consecutive methods wraps only the one after `logged`. ## Why interviewers ask This is a senior question about hook timing: the candidate should know that `method_added` fires after the definition, that the hook's own definitions trigger it, and how to break the cycle.

  • Why does defining the wrapper inside a prepended Module.new not trigger the class's method_added?
    `method_added` is called on the module or class where the method is stored. `define_method` inside `Module.new` stores the method in that anonymous module, so only the module's own (default, no-op) `method_added` runs; `Report`'s hook is not involved.
  • How would the decorator also handle class methods declared with def self.x?
    Those are singleton methods, so implement `singleton_method_added(name)` on the class as well, with the same flag discipline, and wrap on the singleton class. Expect the hook to be called with its own name when it is first defined.

saying these in an interview costs you the question

  • method_added runs before the method exists, so it can veto it.
  • define_method does not trigger method_added.
  • Clearing the flag after define_method is enough to stop recursion.
  • method_added also reports def self.x definitions.
  • A hook that skips super cannot break other libraries.