A dispatcher that selects a method from the tuple of all argument runtime types has to answer questions a single-receiver dispatcher never faces. Name those questions, and explain how real systems such as Julia, Common Lisp's CLOS and Clojure's multimethods answer them.
answer
- applicable = per-position supertype
- incomparable candidates: better in one slot, worse in another
- Julia errors, CLOS orders left-to-right, Clojure needs prefer-method
- type piracy vs Rust's orphan rule
- no vtable slot: cache on the class tuple or JIT-specialise
basics
~20 sSpecificity over tuples is only a partial order, so two candidates can be incomparable — neither is more specific than the other. Julia reports an ambiguity error at the call, CLOS imposes a total order by leftmost argument, Clojure demands an explicit preference. Method tables also become open and unowned.
solid answer
~50 sThree problems, none of which arises when one receiver decides. - **Ordering.** Applicability is per-position subtyping; comparing two candidates position by position can leave them incomparable — for a call of `(Int, Int)`, neither `(Int, Number)` nor `(Number, Int)` is more specific. - **Julia** reports a `MethodError` naming the ambiguity at the call, and expects you to define the more specific `(Int, Int)` method. **CLOS** never reports it: argument precedence order makes the leftmost parameter dominate, producing a total order whose answer silently depends on parameter order. **Clojure** refuses to choose and requires `prefer-method`, over hierarchies you built with `derive`. - **Ownership.** Methods live outside the classes, so any module can add one for your type pair — Julia names the abuse "type piracy"; Rust's orphan rule bans it outright to keep implementations coherent; Clojure protocols and Swift retroactive conformance permit it. - **Implementation.** No fixed vtable slot exists: CLOS caches on the class tuple, Julia specialises per concrete tuple and invalidates compiled code when methods are added.
code
julia · 6 linesf(a::Int, b::Number) = 1
f(a::Number, b::Int) = 2
f(1, 1)
# ERROR: MethodError: f(::Int64, ::Int64) is ambiguous
# fix: define f(a::Int, b::Int) -- the intersectiongo deeper
Know that when more than one argument type participates in selection, two methods can both apply and the system needs a rule to choose.
Be able to give the incomparable example — (Integer, Number) versus (Number, Integer) for two integers — and name one system that errors on it.
Contrast error, positional tie-break and explicit preference as policies, and explain why an open method table costs you class-level ownership.
Weigh coherence against extensibility as a platform decision — orphan rule versus retroactive extension — and account for the compilation cost of an open tuple-keyed table when choosing a dispatch model.
## Why the tuple changes everything With one receiver, selection is a walk up a single inheritance chain: the first matching implementation wins, and the chain gives a total order for free. Once selection considers a *tuple* of argument types, the ordering that made this trivial disappears, and three genuinely new problems appear. ## Problem 1: specificity is only a partial order A method is *applicable* to a call if each parameter type is a supertype of (or equal to) the corresponding argument's runtime type. Comparing two applicable methods means comparing them position by position. Method A beats method B only if A is at least as specific in every position. When A is more specific in one position and B is more specific in another, neither dominates: they are incomparable under the pointwise comparison, and the system has to do something. The canonical case: methods declared for `(Integer, Number)` and `(Number, Integer)`, and a call with two integers. Both apply. Neither is more specific than the other. **Julia** treats this as a program defect and says so: the call raises a `MethodError` that names both candidates as ambiguous, and the fix is to add the method for the intersection, `(Integer, Integer)`. Julia will also warn about ambiguities across packages, because a package you did not write can create one by adding a method. **CLOS** never reports the problem, because it defines it away. Applicable methods are sorted using *argument precedence order*, which by default makes the leftmost parameter dominant: parameter 1 decides, and only exact ties fall through to parameter 2, and so on. That is a total order, so a winner always exists — but the winner depends on the order you happened to write the parameters in. CLOS also lets you declare a different precedence order per generic function, which makes the dependency explicit rather than accidental. **Clojure** takes a third position: `defmulti` will throw when two dispatch values are both applicable and neither is preferred, and you resolve it with `prefer-method`. Its hierarchy is not the class graph at all — you build it with `derive`, so the ordering questions apply to values you invented as well as to types. Design-wise these are three defensible answers to the same lattice problem: report it, define it away with a positional tie-break, or require an explicit human decision. ## Problem 2: the method table is open and unowned In a single-dispatch language a method is inside a class, so the class is the unit of ownership and encapsulation. A multimethod is not inside anything. Any module can add a method for any tuple of types, including types it does not own and a generic function it does not own. The Julia community calls the abusive case *type piracy*: defining a method whose function and whose argument types all belong to other packages, so loading your package silently changes another package's behaviour. Different ecosystems price this differently. Rust's **orphan rule** forbids implementing a foreign trait for a foreign type, guaranteeing that a type has at most one implementation of a trait program-wide — coherence bought at the cost of needing newtype wrappers. Swift permits **retroactive conformance**, buying extensibility and accepting that two libraries can conform the same type in incompatible ways. Clojure's `extend-type` sits with Swift. This is the single most consequential trade in the area, and it is a language-policy decision rather than a technical necessity. ## Problem 3: you cannot compile it to a slot index Single dispatch compiles to a table lookup at a fixed index, known at compile time. Tuple dispatch cannot: the applicable set depends on a combination of runtime types, and the set of methods can grow while the program runs. CLOS implementations answer with caching: the first call with a given tuple of classes computes the effective method and memoises it keyed on that tuple, so subsequent calls are a hash lookup. Julia answers with JIT specialisation — it compiles a version specialised to the concrete argument types it actually sees, so hot call sites become direct calls. Because methods can be added later, Julia additionally tracks a *world age*: adding a method invalidates already-compiled code that assumed the old method table, and the affected functions are recompiled. That machinery is the runtime price of an open, tuple-keyed dispatch table. ## How to answer this in an interview Lead with the ordering point, because it is the one candidates almost never have language for: applicability is per-position, specificity is a partial order, and incomparable candidates are a real possibility rather than an edge case. Then give the three answers — error, positional tie-break, explicit preference — with the systems that implement each. Close on ownership, since that is what actually bites teams: the same openness that makes multimethods extensible removes the class as the unit of encapsulation.
- How do you fix an ambiguity that Julia reports at a call?Define the method for the intersection of the ambiguous signatures — for methods on (Integer, Number) and (Number, Integer), add one for (Integer, Integer). That method is strictly more specific than both, so it dominates and the ambiguity disappears. Broadening one of the existing signatures also works but usually changes behaviour for other calls, so the intersection method is the standard fix.
- What does CLOS's argument precedence order cost you, given that it never reports ambiguity?It makes parameter order semantically load-bearing: swapping two parameters in the generic function's lambda list can change which method a call selects, with no error anywhere. You gain a guaranteed winner and lose the diagnostic. CLOS mitigates this by letting each generic function declare its own precedence order explicitly, so the dependency at least becomes visible in the source.
saying these in an interview costs you the question
- Assuming the most recently defined or the first-defined method wins in every multimethod system.
- Believing the leftmost argument is universally dominant — that is CLOS's rule, not Julia's or Clojure's.
- Thinking ambiguity is a rare corner case rather than a direct consequence of comparing tuples position by position.
- Assuming a multimethod compiles to a fixed vtable slot like a single-receiver call.
- Not seeing that an open method table removes the class as the unit of ownership, so a third-party package can change your behaviour.