Some languages treat a call written as receiver.doThing() as a message the receiver resolves at run time; others compile it into a fixed member call. Contrast the two models using specific languages, and say what each buys and costs.
answer
- selector is data vs vtable slot index
- doesNotUnderstand: / method_missing / __getattr__
- respond_to_missing? or introspection lies
- Go has no hook: mocks are codegen
- Python dunders resolve on the type, not the instance
basics
~20 sUnder messaging (Smalltalk, Objective-C, Ruby, Python) the method name is a runtime value the receiver resolves, so an unknown name reaches a hook: doesNotUnderstand:, method_missing, getattr. Under member calls (C++, Go, Rust) the target is fixed at compile time and an unknown name does not compile.
solid answer
~1 minTwo different questions are being answered: who decides what this call means, and when. - **Smalltalk / Objective-C** — the selector is a runtime value. If the receiver has no method for it, control reaches `doesNotUnderstand:` / `forwardInvocation:`, which is how `NSProxy` and distributed-object stubs exist at all. Price: a cached dictionary lookup per send, and the runtime crash `unrecognized selector sent to instance` for a mistake other languages catch while compiling. - **Ruby / Python** — the same hook renamed: `method_missing` (plus `respond_to_missing?`, which you must also write or introspection lies about the object) and `__getattr__`, which fires only after normal lookup fails. Python deliberately narrows the power: operator dispatch is looked up on the type, so `__getattr__` cannot fabricate `__len__`. - **C++ / Rust / Go** — the name is erased. A C++ virtual call is a vtable slot index, so there is nothing to intercept; Go checks method sets against interfaces at compile time and has no missing-method hook, which is why Go mocks are code generation rather than runtime magic. You buy devirtualization, inlining and completeness in tooling. - **JavaScript** is the middle case: calls are property lookups, interceptable only through a `Proxy` created around the object up front — you cannot retrofit interception onto a reference someone already holds. So: messaging when the receiver set is open (ORMs, RPC stubs, DSLs); member calls when you want the compiler and the IDE to know the answer.
code
ruby · 11 linesclass Row
def method_missing(name, *args)
return @cols[name] if @cols.key?(name)
super
end
def respond_to_missing?(name, priv = false)
@cols.key?(name) || super
end
end
row.email # works; no such method was ever definedgo deeper
Be able to say that in some languages the method name still exists while the program runs and in others it does not, and give one example of each.
Name the actual hooks (doesNotUnderstand:, method_missing, getattr) and the fact that Go and C++ have none, plus one consequence such as mocking style.
Discuss the price of each model: runtime crashes and introspection drift versus code generation and the inability to proxy, and pick a side for a given open-or-closed receiver set.
Frame it as where verification lives. Choosing messaging pushes contract checking into tests and runtime observability; choosing member calls pushes flexibility into build-time generation. Argue the choice from how open the system's object population is.
## The claim under the syntax Every object language writes calls the same way, so the syntax hides a real fork. In one family, `receiver.name(args)` means *send the symbol `name` to this object and let it decide*. In the other it means *jump to the entry the compiler already picked for this static type*. Everything else in this question follows from that fork. ## Messaging: the name survives to run time Smalltalk's model is that an object is a black box that answers messages. The selector (`printOn:`, `at:put:`) is a first-class value; sending is a lookup in the receiver's method dictionary, walking up its superclass chain, with an inline cache to make it affordable. If the walk fails, the runtime does not error — it re-sends `doesNotUnderstand:` with the original message reified as an object. That single hook is the whole basis of proxies. Objective-C inherits this directly: `objc_msgSend` takes a `SEL`, and unhandled selectors get a forwarding pipeline (`forwardingTargetForSelector:`, then `forwardInvocation:`). `NSProxy` is a root class with almost no methods precisely so that everything falls into forwarding. Ruby's `method_missing` and Python's `__getattr__` are the same idea in dynamic-language clothing: ActiveRecord's `find_by_email` and countless RPC stubs are methods that never existed until called. The cost has three parts. First, performance: a send is a lookup plus caching, not a call. Second, honesty of introspection: in Ruby, if you implement `method_missing` without `respond_to_missing?`, then `respond_to?` says the object cannot do something it demonstrably does, and serializers, documentation tools and completion all inherit the lie. Third, the failure moves to run time — Objective-C's `unrecognized selector sent to instance` is a production crash for a typo. ## Member calls: the name is erased In C++ a non-virtual call is a direct call and a virtual call is an indexed load from a vtable. After compilation the string `doThing` is not present in any dispatch path. There is therefore no interception point: a proxy in C++ is a class that mechanically re-declares every member of the interface and forwards it, which is why templates and code generation carry that weight. Go goes further: interface satisfaction is structural but checked when compiling, and there is no fallback for an absent method. Rust resolves methods against traits in scope, and even the ability to add a method to a foreign type is constrained by the orphan rule. What these languages buy is that the compiler and the tooling always know the full answer, and the optimizer can devirtualize and inline. ## The middle ground JavaScript looks dynamic but is not a messaging language: a call is a property read followed by an invoke. Interception exists only through `Proxy`, and a `Proxy` must wrap the object before anyone gets a reference to it — there is no `doesNotUnderstand:` on a plain object. Python is deliberately half-open in a different way: `__getattr__` catches ordinary attribute misses, but special (dunder) methods are looked up on the type, so a proxy that forwards `__len__` or `__add__` must define them explicitly. ## Why interviewers care Because it explains why a mocking library looks like a one-liner in Ruby and a code generator in Go, and why an ORM can invent query methods in one ecosystem and requires a build step in another. It also frames a real design decision: a system with an open receiver set (plugins, remote objects, schemas discovered at run time) fights the compiler in the member-call family, while a system that needs static verification and IDE navigation fights the runtime in the messaging family. ## Responsibilities restated The messaging view has a modelling consequence worth saying out loud: an object's responsibilities are the set of messages it answers, not the fields it holds. In a messaging language that set can genuinely be computed at run time; in a member-call language the set is fixed when compiling and therefore *is* the declared interface. Both are the same idea, but only one of them can change after the program starts.
- A Ruby object answers calls through method_missing but respond_to? returns false for them. What breaks, and what is the fix?Anything that asks before calling: duck-typing checks, serializers, form builders, delegation helpers and REPL completion all conclude the object cannot do it. The fix is to implement respond_to_missing? (and often __dir__ or methods) alongside method_missing so introspection and behaviour agree. Objective-C has the same obligation via respondsToSelector:.
- How do C++ or Go get proxy-like behaviour without a missing-method hook?They move the work to compile time. C++ uses templates or generated forwarding classes that re-declare each member; Go generates mock types from an interface, or embeds an interface value in a struct so the struct satisfies the interface while unimplemented methods panic if actually called. Both approaches give you the compiler's guarantee back in exchange for a build step.
saying these in an interview costs you the question
- Saying every object language looks a method up by name at run time — C++, Rust and Go erase the name entirely.
- Claiming method_missing or __getattr__ is free; it costs lookup, tooling accuracy and static verification.
- Assuming a Python __getattr__ or a JS Proxy will intercept operator/dunder dispatch on the type.
- Thinking an unknown message quietly returns nil rather than reaching a hook or raising.
- Calling JavaScript a message-passing language in the Smalltalk sense — it has property lookup plus Proxy, not a doesNotUnderstand hook.