An operation's behaviour depends on the runtime types of TWO objects, not one — for example what happens when two different kinds of game entity collide, or how a document node is rendered onto a particular device. Explain what a single-dispatch language can and cannot decide for you here, and how languages differ in what they offer.
answer
- one runtime type selected, the rest declared
- Julia/CLOS: table keyed by the argument tuple
- Clojure defmulti: dispatch value is any function
- singledispatch = first argument only
- two hops resolve two types, at the cost of a wide interface
basics
~20 sSingle dispatch consults exactly one runtime type — the receiver — so the second object's runtime type is invisible to selection. Julia and Common Lisp key methods on the whole argument tuple, Clojure lets you name the dispatch function, and elsewhere you bounce through a second call on the other object.
solid answer
~60 sSelection uses one runtime type; every other argument contributes only its declared type. Languages diverge on how much of that gap they close. - **Julia**: `collide(a::Asteroid, b::Ship)` is one entry in a table keyed by the tuple of runtime types. Adding a new pair is a new top-level method that touches no class — and no class owns the operation any more. - **CLOS**: `defmethod` specialises several parameters at once, combining applicable methods with `call-next-method`. - **Clojure**: `defmulti` takes an arbitrary dispatch function, so you can key on both classes or on data; ties need an explicit `prefer-method`. - **Python**: `functools.singledispatch` is single by construction — it examines the first argument only. - **C#**: casting one argument to `dynamic` defers binding to runtime, buying two-type selection in one line and paying with `RuntimeBinderException` instead of a compile error. - **Manual technique**: the receiver call resolves the first type, then calls back on the second object passing itself, so two dynamic hops resolve two types. The catalogue name for that shape belongs to the design-patterns tree.
code
julia · 8 linesstruct Asteroid end
struct Ship end
collide(a::Asteroid, b::Ship) = "boom"
collide(a::Ship, b::Asteroid) = "dodge"
collide(a::Any, b::Any) = "nothing"
collide(Asteroid(), Ship()) # "boom" -- both types consultedgo deeper
Know that a normal method call picks its body from one object's runtime type, and that the other arguments do not participate in that choice.
Be able to describe the two-hop technique in mechanism terms and name a language whose dispatch already covers the whole argument tuple.
Choose between two-hop dispatch, a type switch and a genuine multimethod based on which axis will grow, and state what each gives up.
Frame it as open method tables versus closed variant sets: extensibility with runtime-only failure reporting against exhaustiveness with compile-time coverage, and pick per the stability of the type sets involved.
## What single dispatch actually decides In a mainstream object-oriented language a call selects a method from **one** runtime type: the receiver's. Everything else about the call — how many arguments there are and what they are declared to be — is fixed before the program runs. So an operation whose correct behaviour depends on *two* runtime types has half its selection performed by the language and half left to you. That is not a defect in any single language; it is a definition. "Single dispatch" names the choice to make one participant privileged. The interesting material is what different languages do with the other participants. ## Languages that dispatch on the tuple **Julia** is built on multiple dispatch. A method is an entry in a table keyed by a tuple of parameter types, and a call selects the most specific applicable entry using the runtime types of all positional arguments. `collide(a::Asteroid, b::Ship)` and `collide(a::Ship, b::Asteroid)` are two ordinary methods; adding a third entity type means adding methods, not editing existing ones. **Common Lisp's CLOS** does the same with `defmethod`, and adds method combination: `:before`, `:after`, `:around` methods and `call-next-method` let you compose the applicable set instead of choosing exactly one body. **Clojure's `defmulti`** generalises further: the dispatch *value* is whatever a user-supplied function returns. Dispatching on `[(class a) (class b)]` gives you type-pair dispatch; dispatching on a field value gives you something no type-based system offers. Hierarchies for these values are user-defined with `derive`, not read off the class graph. **Groovy**, running on the JVM, resolves argument types at runtime, so an ordinary set of same-named methods behaves like a small multimethod table — with the corresponding shift of selection failures from compile time to runtime. **C#** offers a targeted escape hatch: mark one argument `dynamic` and the binder re-runs selection at runtime for that call site. It is the shortest route to two-type selection in a statically bound language, and it converts a compile-time error into `RuntimeBinderException`. **Python** deliberately stops at one: `functools.singledispatch` is documented as generic-function dispatch on the *first* argument, and third-party libraries exist precisely because the standard library declined to go further. ## The manual technique When the language gives you none of the above, you can still resolve two types with two ordinary single dispatches. The first call goes to one object, which resolves that object's runtime type. Inside that body, the object's own type is now statically known, so it calls a second, differently-named operation on the other object, passing itself. The second call resolves the second runtime type — and the body it lands in knows both. Two dynamic hops, two types resolved. The cost is structural, not syntactic: the second object's interface must contain one entry per possible first type. Adding a new kind of first type therefore edits every implementation of that interface. That is the coupling that makes this technique a design decision rather than a trick, and it is why the catalogue pattern built on it (covered under design patterns, not here) is always discussed together with how stable the type set is. ## The alternative: match on the pair Instead of arranging for the language to dispatch twice, you can inspect both types yourself in one place — a type switch, or a pattern match over a tuple. Rust matching a `(Shape, Device)` tuple of enum variants, or Scala matching a pair of sealed-trait cases, gets something none of the dispatch mechanisms above provide: the compiler checks that you covered every combination and names the ones you missed. Julia and Clojure cannot offer that, because their method tables are open by construction — any module may add an entry later, so no closed set exists to check against. The trade is therefore not "clean versus hacky". It is: open method table with runtime-only ambiguity reporting (Julia, CLOS, Clojure), versus closed variant set with compile-time exhaustiveness (Rust, Scala, Kotlin), versus two-hop manual dispatch that keeps the operation extensible but freezes the participant types. ## What interviewers are checking That you can state the limitation precisely — one runtime type, the rest declared — rather than as "polymorphism does not work here"; that you know at least one language where the limitation simply does not exist; and that you can describe the two-hop technique in mechanism terms, including which axis it makes expensive.
- What does the two-hop technique cost when a new participant type is introduced?The second object's interface has one entry per possible first type, so a new first type adds a member to that interface and forces an edit in every implementation of it. The operation set stays cheap to extend — you can add whole new operations without touching the participants — but the participant set becomes expensive. That asymmetry is the whole reason to choose or reject the technique.
- Why can a pattern match over a pair of types offer exhaustiveness checking when Julia's dispatch cannot?Exhaustiveness requires a closed set of possibilities known to the compiler, which sealed hierarchies and enums provide. Julia's method table is open: any package may add a method for a new type pair at any time, so there is no complete set to check against. The openness that makes multiple dispatch extensible is exactly what makes static coverage checking impossible.
saying these in an interview costs you the question
- Saying the language 'cannot do polymorphism on two types' without distinguishing single dispatch from multiple dispatch as language designs.
- Believing every OO language resolves arguments by their runtime types, then being surprised by a statically bound selection.
- Assuming Python's functools.singledispatch examines all arguments.
- Presenting the two-hop technique as free, ignoring that it freezes the participant type set.
- Reaching for reflection or instanceof chains without considering that the language may already have a dispatch mechanism (Clojure defmulti, C# dynamic).