In Ruby, how do you write method_missing for a settings object that answers any config key as a method, and why must it call super?
answer
- match only names you handle
- check argument count too
- bare super for the rest
- typos must still raise
- stack level too deep
basics
~20 sOverride method_missing to return the stored value when the name is a known key, handle key= writers, and call bare super for every other name. Without super, typos return nil silently instead of raising NoMethodError.
solid answer
~40 sI keep the values in a Hash and override `method_missing(name, *args, &block)`: if `name` is a known key and no arguments were given, return the value; if it ends in `=` with one argument and names an existing key, store it; otherwise call bare `super`, which forwards the same name, arguments and block to the default handler. That `super` matters: without it an unknown name like `settings.prot` returns `nil` and the bug surfaces far away, whereas `super` raises the usual `NoMethodError` with the caller's line. I pair it with `respond_to_missing?`, avoid calling any undefined method inside it, since that recurses until `SystemStackError`, and reject keys such as `display` or `freeze` that real methods would shadow.
code
ruby · 11 linesclass Settings
def initialize(values) = @values = values
def method_missing(name, *args)
@values.fetch(nme) { super } # typo: nme
end
end
Settings.new({}).port
# nme is itself a missing name, so method_missing calls itself
# until SystemStackError: stack level too deepgo deeper
Recall the shape: match the names you handle, answer them, call super for the rest.
Explain what bare super forwards, why skipping it hides typos, and how argument checks separate readers from writers.
Show production judgement: the recursion trap, keys shadowed by real methods, and choosing Struct, Data or fetch when names are not open-ended.
Decide whether ghost-method convenience is worth the lost discoverability for shared configuration objects across a codebase.
## The goal A settings object loaded from a YAML or JSON file holds keys such as `port`, `host` and `timeout`. Callers would rather write `settings.port` than `settings["port"]`. The keys are not known when the class is written, so there is no method to define in advance; `method_missing` can answer them. ## A correct implementation ```ruby class Settings def initialize(values) = @values = values.transform_keys(&:to_s) def method_missing(name, *args, &block) key = name.to_s if key.end_with?("=") && args.size == 1 && @values.key?(key.chomp("=")) @values[key.chomp("=")] = args.first elsif @values.key?(key) && args.empty? @values[key] else super end end def respond_to_missing?(name, include_private = false) key = name.to_s.chomp("=") @values.key?(key) || super end end settings = Settings.new(port: 8080, host: "db") settings.port # => 8080 settings.port = 9090 settings.prot # NoMethodError: undefined method 'prot' for an instance of Settings ``` The shape to remember: 1. **Recognise** only names you really handle, and check the arguments too: a reader takes none, a writer exactly one and only for an existing key. 2. **Answer** those names. 3. **Call `super`** for everything else, with no arguments, so the original name, arguments and block go to the default handler. 4. **Pair it with `respond_to_missing?`**, so `respond_to?` and `method` agree with what `method_missing` answers. ## Why super is not optional Without `super`, the last branch would return `nil` or some other value for every unknown name. A typo such as `settings.prot` would then quietly return `nil`, and the failure would surface later and far away, as a `nil` port passed to a socket. Calling `super`: - raises the normal `NoMethodError` with the real name and the caller's line in the backtrace; - keeps any ancestor's `method_missing` working, for example one added by a module included in `Settings`; - preserves the **error class**: a bare identifier inside the class still gives `NameError`, because the default handler knows how the original call was written. Bare `super` passes along exactly what `method_missing` received. If you narrow the arguments by hand, use `super(name, *args, &block)` rather than dropping some. ## Traps specific to method_missing | Trap | What happens | Fix | |---|---|---| | Calling an undefined method inside `method_missing` | it calls `method_missing` again, forever, until `SystemStackError` ("stack level too deep") | use only defined methods and instance variables inside it | | A key named like a public method (`display`, `freeze`, `hash`) | the real method runs; `method_missing` is never reached | reject such keys at load time, or read with `[]` | | A key named like a private `Kernel` method (`format`, `system`) | reaches `method_missing`, because a private method called with a receiver counts as missing | same: validate keys or use `[]` | | Defining `method_missing` on `Object` itself | Ruby warns "redefining Object#method_missing may cause infinite loop" and every object is affected | define it in the one class that needs it | ## Testing it A small set of tests pins the behaviour down: - a known key returns its value, and a writer stores a new one; - an unknown name raises `NoMethodError` with that name (`error.name == :prot`); - `respond_to?` agrees with `method_missing` for a known key, a writer and an unknown name; - a reader called with an argument is refused, so `settings.port(1)` does not silently ignore it. ## When not to use it For a fixed set of fields, `Struct` or `Data.define` gives real methods, real `respond_to?` answers and better speed. `method_missing` is for names that truly are open-ended, like keys in a loaded file. Even then, a plain `fetch("port")` API is simpler; ghost methods are a convenience that must be earned. ## What an interviewer is listening for The expected answer shows a `method_missing` that matches only the names it handles, returns their values, and calls `super` otherwise, explaining that skipping `super` turns typos into silent `nil`s. Strong answers mention `respond_to_missing?`, argument checks for readers and writers, the recursion trap and keys that collide with real methods.
- What is the difference between bare super and super(name) inside method_missing?Bare `super` forwards exactly what `method_missing` received: the name, every argument and the block. `super(name)` sends only the name, so the default error loses the arguments (`NoMethodError#args` is empty) and an ancestor's handler that needs them gets nothing. Use bare `super`, or pass all of `name, *args, &block` explicitly.
- Why does settings.display print the object instead of returning the display key?`display` is a public `Kernel` method, so lookup finds it and `method_missing` is never called. Ghost methods only answer names that no ancestor defines publicly. Keys that collide with real methods must be rejected when loading, renamed, or read through `[]`.
method_missing is a receptionist covering an empty desk: she answers questions she can, and passes every other caller to the switchboard instead of inventing an answer.
saying these in an interview costs you the question
- returning nil for unknown names is fine, callers can check
- super inside method_missing is only needed with inheritance
- every key, including display or freeze, reaches method_missing
- method_missing can safely call helper methods it has not defined
- super(name) forwards the arguments and block as well