Multiple dispatch

RawGraph

Last edited

Fact-checked

In review queue

Sources

24 citations

Revision

v2 · 3,635 words

Fact-checks are independent of edits: a reviewer re-verifies the article against its sources and stamps the date. How we verify

Multiple dispatch (also called multimethods) is a programming-language feature in which the implementation of a function or method is selected at run time from the types of all of its arguments, not just one privileged receiver [1][22]. As the Julia language documentation puts it, "Using all of a function's arguments to choose which method should be invoked, rather than just the first, is known as multiple dispatch" [22]. It generalizes the more familiar single dispatch model of conventional object-oriented programming, where method selection considers only the type of one receiver object.

Multiple dispatch is the central organizing principle of object-oriented programming in Common Lisp's CLOS, Dylan, Julia, Cecil, and several other languages [2][3]. The mechanism addresses the expression problem that troubles both functional and conventional OO designs, and is especially valuable in scientific computing, symbolic computation, machine learning, and AI knowledge representation [4][5]. Julia, released publicly in 2012 and downloaded more than 3 million times across over 1,500 universities by 2018, has popularized multiple dispatch outside the Lisp community by showing that specialization can make multimethods perform comparably to statically dispatched code [4][23].

What is multiple dispatch?

The word "dispatch" refers to how a language runtime decides which concrete implementation of an operation to invoke. Disciplines differ in two axes: how many arguments participate, and whether the decision is made at compile time or run time.

  • Single dispatch. The implementation is selected on the type of one privileged "receiver." In Java, C++, Smalltalk, Python, and most mainstream OO languages, obj.method(args) looks up method in a virtual table tied to the runtime class of obj. The remaining arguments do not affect which body runs.
  • Multiple dispatch. The implementation is chosen on the runtime types of two or more arguments simultaneously. There is no privileged receiver; f(a, b, c) selects the most specific applicable method given the types of all arguments together.
  • Static overloading. Languages such as C++ and Java let several functions share a name and disambiguate by parameter types, but the compiler resolves the call against the static types of arguments [6]. Static overloading cannot react to runtime type information, and is therefore a different mechanism.
  • Predicate dispatch. A generalization in which methods are selected by arbitrary boolean predicates over arguments, not only type tests [7]. Multiple dispatch is the special case where predicates are restricted to type membership.
  • Pattern matching. A mechanism in Haskell, Scala, ML, and Rust where function clauses are guarded by structural pattern matching on argument shape. It is related but distinct: it dispatches on values and constructors rather than a class hierarchy.

The term "multimethod" was popularized by CLOS in 1988, although the idea appeared earlier in Xerox PARC's CommonLoops experiment [8][9].

How is it different from single dispatch?

The difference is which arguments are allowed to influence method selection. Single dispatch uses the runtime type of just the first (receiver) argument; multiple dispatch uses the runtime types of all of the arguments. Python makes the contrast concrete: its standard-library functools.singledispatch decorator, introduced by PEP 443 in Python 3.4 (2014), "dispatches on the type of the first argument" only and is therefore single-dispatch, whereas third-party packages such as multipledispatch and multimethod add true all-argument dispatch [24].

Consider a physics simulation in which two objects collide. The classes are Asteroid and Spaceship, and collide(x, y) produces a different effect depending on which kinds of object collide.

In a single-dispatch language this is awkward. The call x.collide(y) selects on the class of x only, so the method body must inspect y with instanceof or isinstance checks, or the programmer must encode a visitor pattern. Both approaches scale poorly as more classes are added.

In a multiple-dispatch language the four cases are written directly:

collide(::Asteroid,  ::Asteroid)  = "shatter both"
collide(::Asteroid,  ::Spaceship) = "asteroid impacts hull"
collide(::Spaceship, ::Asteroid)  = "asteroid impacts hull"
collide(::Spaceship, ::Spaceship) = "ships collide"

The runtime examines the types of both arguments and selects the matching method. No if ladder is needed; adding a new class such as SpaceStation only requires new method clauses. Multiple dispatch treats all arguments as equal participants in the dispatch decision.

The expression problem

In 1998 Phil Wadler popularized the term "expression problem" to describe a long-standing tension in language design [10]. The problem asks how to build a system in which programmers can (1) add new data types, (2) add new operations on those types, (3) without recompiling or modifying existing code, and (4) while retaining static type safety.

Conventional OO makes (1) easy and (2) hard: adding a subclass is local, but adding a new operation requires editing every class. Functional programming organized around algebraic data types and pattern matching has the opposite weakness: a new operation is just a new function, but a new constructor forces every existing function to add a clause.

Multiple dispatch makes both new types and new operations external to existing definitions. A new type can be introduced together with method clauses for any existing generic function, and a new generic function can be introduced together with methods covering any existing types. Neither extension requires modifying the original sources [4][10].

Which languages support multiple dispatch?

Several languages support multiple dispatch as a first-class feature; many others provide it through libraries.

LanguageYearMechanismNotes
Common Lisp (CLOS)1988defgeneric, defmethodCanonical multimethod system with method combination [2]
Dylan1992Generic functions and methodsApple project; only OO model [11]
Cecil1992Multimethods, predicate guardsResearch language by Craig Chambers [7]
Diesel2002MultimethodsCecil's successor
Nice1999Statically typed multimethodsJVM bytecode
Julia2012Generic functions, JIT-specializedPrimary paradigm; fast via specialization [4]
Raku (Perl 6)2015multi sub, multi methodNative multi-dispatch on type and arity [12]
Clojure2007defmulti/defmethodDispatch on user-defined function [13]
Groovy2003Runtime overload resolutionDynamic dispatch on runtime types
C#2010+dynamic keywordMultimethod-like since 4.0 [14]
Pythonn/amultimethod, multipledispatch libsfunctools.singledispatch is single-only [24]
Haskell1990Type classesCompile-time, parametric form
Tiny CLOS1992LibraryCLOS-style multimethods for Scheme

Elixir and Erlang dispatch function clauses by pattern matching on values, which is multi-argument and dynamic but not type-driven. Tim Sweeney's game language Skookumchuck, in his 2006 POPL keynote "The Next Mainstream Programming Language," also included multimethod dispatch [15].

CLOS multimethods

The CLOS specification by Daniel Bobrow, Linda DeMichiel, Richard Gabriel, Sonya Keene, Gregor Kiczales, and David Moon introduced multimethods to a wide audience of Common Lisp programmers in 1988 [2]. CLOS treats methods not as belonging to a class but to a generic function, an object holding a collection of methods specialized on tuples of argument types.

The principal forms are defgeneric, which declares a generic function, and defmethod, which adds a single specialized clause. When the function is called, CLOS computes the applicable methods by checking each method's specializers against the actual argument classes, then orders them by specificity using each argument's class precedence list (CPL), a linearization of the multiple-inheritance graph. The most specific method runs first, and call-next-method chains to the next [16].

CLOS also supports method combination. Methods can be qualified :before, :after, or :around, which run unconditionally alongside the primary method. Custom combinations such as progn, +, min, max, append, and nconc let the language combine results from many methods, a feature often used in framework design. A logging :before method, for example, runs whenever any specialization of a generic function fires, without modifying the primary methods.

Why does Julia use multiple dispatch?

In Julia, designed by Jeff Bezanson, Stefan Karpinski, Viral Shah, and Alan Edelman, multiple dispatch is the only object-oriented mechanism the language provides [4]. There are no classes with bound methods. Every function is generic, and the runtime selects a method by examining the concrete types of all positional arguments. Work on the language began in 2009 and the first public release came on 14 February 2012; the four co-creators received the 2019 James H. Wilkinson Prize for Numerical Software for the design [23].

The authors of "Julia: A Fresh Approach to Numerical Computing" frame the language as "a dance between specialization and abstraction," in which "Multiple dispatch, a technique from computer science, picks the right algorithm for the right circumstance" [4]. They argue that "This paradigm is a natural fit for numerical computing, since so many important operations involve interactions among multiple values or entities" [4], for example arithmetic between an integer and a floating-point number, where neither operand is a more natural receiver than the other.

Julia compiles each method specialization to native code on demand using its JIT compilation infrastructure built on LLVM. The first call to f(x::Int, y::Float64) triggers the compiler to generate a version specialized to those argument types. Later calls with the same signature reuse the compiled method directly, with no dispatch overhead beyond a cached method-table lookup. The dispatch decision is logically dynamic, but the executed code is essentially monomorphized [4].

Methods can be specialized on abstract types, concrete types, parametric types, and type unions. Dispatch operates on positional arguments only, not on keyword arguments, which keeps the method-table semantics tractable.

In his 2019 JuliaCon talk "The Unreasonable Effectiveness of Multiple Dispatch," Karpinski argued that multiple dispatch is unusually effective at enabling package composability [5]. Because methods belong to generic functions rather than classes, an unrelated package can extend an existing function with new methods for its own types. A DataFrame from one package accepts statistics from another, plotting from a third, and gradient-based optimization from a fourth, without any of those packages having anticipated the others. Karpinski emphasized that this was empirical: the community discovered that multiple dispatch reused existing definitions across packages far more often than the designers had expected.

How fast is multiple dispatch?

The cost of dispatch depends on how much work the runtime does to pick a method.

  • Compile-time overloading in C++ and Java is free at run time. The generated code is a direct call.
  • Single dispatch in conventional VMs is cheap: a virtual call reads a function pointer from a per-class table.
  • Naive multiple dispatch is more expensive because the system must intersect the applicable-method set across all argument types and order it by specificity. A table indexed by tuples would be exponential in the number of arguments.
  • Inline caching mitigates this by remembering, at each call site, the most recent dispatch decision. Most call sites see only a few type combinations, so cache hits avoid the full search.
  • Method specialization is Julia's approach: the compiler generates separate native code for each combination of types observed at run time, treating each specialization as an independent statically dispatched function [4].
  • Discrimination networks and tree-shaped lookup are alternatives studied in the CLOS and Cecil communities [7].

CLOS implementations such as SBCL combine generic-function caches with type-discriminating networks, and Julia's specialization makes hot dispatch in numerical code competitive with hand-written C [4].

ConceptWhen resolvedSelection criterionTypical languages
Single dispatchRun timeType of one receiverSmalltalk, Java, Python, Ruby
Multiple dispatchRun timeTuple of argument typesCLOS, Dylan, Julia
Predicate dispatchRun timeArbitrary boolean over argumentsCecil, Diesel
Pattern matchingCompile or run timeStructural shape of valuesHaskell, Scala, Rust, ML
Type classes / traitsCompile timeConstraints on type parametersHaskell, Rust
Method overloadingCompile timeStatic argument typesC++, Java, C#
Visitor patternRun timeTwo indirections through single dispatchJava, C++
defmulti (Clojure)Run timeResult of arbitrary dispatch functionClojure

Multiple dispatch is dynamic, type-driven, and symmetric across arguments. Predicate dispatch is strictly more general but harder to implement efficiently. Pattern matching is comparably expressive but usually closed: each function lists clauses in one place, whereas multimethods can be extended externally. Type classes give similar extensibility at compile time but require the dispatched value's type to be known statically.

Why is it useful for scientific computing and AI?

Multiple dispatch is a good fit for scientific computing, symbolic computation, machine learning, and AI knowledge representation. As the Julia designers note, the paradigm fits numerical work "since so many important operations involve interactions among multiple values or entities" [4].

Numerical type promotion. Numerical libraries must handle every combination of argument types (Int + Float64, Rational + Complex, and so on). With multiple dispatch each combination is one method, and a standard-library promotion mechanism reduces N x N cases to a few base rules. The same is awkward in single-dispatch languages because either x or y must asymmetrically be the receiver.

Symbolic algebra. Computer algebra systems in Common Lisp, Dylan, and Julia express simplification rules as multimethods. simplify(::Sum, ::Sum) and simplify(::Product, ::Sum) are each separate methods, declared wherever the operator types are defined.

Composable scientific libraries. Julia's DifferentialEquations.jl, Flux.jl, and JuMP.jl interoperate without explicit adapters because they extend the same generic functions on shared abstract types. A custom AbstractMatrix subtype works with linear solvers, automatic differentiation, and plotting from independent packages [4][5].

Machine learning and automatic differentiation. In machine learning frameworks, multiple dispatch lets a single set of generic operators run unchanged across plain arrays, GPU arrays, and dual numbers carrying derivatives. Julia's Flux.jl and the Zygote/ChainRules autodiff stack rely on this: a new numeric type (for tracking gradients, intervals, or units) becomes differentiable simply by adding methods to existing operators, rather than reimplementing the model [5].

AI knowledge representation. CLOS-based AI systems including Cyc and earlier expert system shells use multimethods for inference rules. Adding a new fact type only requires methods for rules that should observe it.

Domain-specific languages. Operator overloading combined with multiple dispatch makes it easy to extend arithmetic and comparison operators across user-defined types, heavily used in automatic differentiation, interval arithmetic, and units of measure.

What are the limitations of multiple dispatch?

Multiple dispatch comes with trade-offs that single-dispatch languages mostly avoid.

  • Larger search space. Methods are indexed by tuples of types, so candidate signatures grow combinatorially. Programmers must reason about how their methods compose with all others on the same generic function.
  • Method ambiguity. With multiple inheritance, methods like f(::A, ::AB) and f(::AB, ::A) can both match f(ab, ab). Julia raises a method-ambiguity error and asks for an explicit tie-breaker f(::AB, ::AB); CLOS uses the class precedence list but can still produce ambiguous orderings [16].
  • Static type checking is harder. Type inference must reason about the union of all method signatures, and the inferred result type may depend on which method is selected. Cecil and Nice explored sound static type systems for multimethods, often requiring all methods of a generic function to be declared in a single module [7].
  • Discoverability. In a single-dispatch language a programmer asks "what methods does this class have?" In a multi-dispatch language the question is "what generic functions accept this type?" Julia provides methodswith and which, and CLOS environments offer similar tools, but newcomers find it harder to navigate large codebases.
  • Tooling and encapsulation. Rename and find-references are harder when methods are not lexically bound to a class. Single dispatch co-locates data and behavior, doubling as an encapsulation boundary; multiple dispatch separates them, so visibility must be enforced through module systems.

Historical context

Multiple dispatch did not appear all at once. Its history runs through the Lisp family and several research projects of the 1980s.

Symbolics Flavors, the object system used on Lisp machines from the late 1970s, was strictly single-receiver but introduced influential ideas about generic functions, mixins, and method combinations [17]. New Flavors added "daemons" for before- and after-method combinations later generalized by CLOS.

In 1986 Daniel Bobrow and colleagues at Xerox PARC presented CommonLoops, the first widely circulated object system to dispatch on multiple arguments [9]. Two years later the merger of CommonLoops, Symbolics's New Flavors, and Lucid's ObjectLisp produced CLOS, codified by the ANSI Common Lisp standard [18]. Guy Steele's Common Lisp the Language (second edition, 1990) popularized the design [19].

By the early 1990s Apple had launched Dylan, a CLOS descendant influenced by writing the original Newton software in CLOS, and Craig Chambers at the University of Washington had begun designing Cecil, a research language using multimethods to investigate static analysis and predicate dispatch [3][7]. Through the 2000s, multiple dispatch was largely confined to research and to the Lisp/Dylan diaspora.

That changed in the 2010s with Julia, which made multiple dispatch the foundation of a numerically focused language with a working JIT compiler [4]. Julia has since become the largest user community for multiple dispatch, reviving interest in the Python scientific stack and Clojure.

Giovanni Castagna's 1995 paper "Covariance and contravariance: conflict without a cause" formalized how subtyping interacts with multimethod selection [6]. Scheme has long supported library-level multimethods through Tiny CLOS [20]; the homoiconicity of Lisp dialects made it easy to retrofit multimethod systems as user-level macros, and earlier work by Steele and Gerald Sussman on first-class procedures laid groundwork for the study of generic functions.

Code examples

The following fragments express the same collide(a, b) multimethod in several languages.

Common Lisp / CLOS.

(defgeneric collide (a b))
(defmethod collide ((a asteroid)  (b asteroid))  "shatter both")
(defmethod collide ((a asteroid)  (b spaceship)) "asteroid impacts hull")
(defmethod collide ((a spaceship) (b asteroid))  "asteroid impacts hull")
(defmethod collide ((a spaceship) (b spaceship)) "ships collide")

Clojure, dispatching on a vector of class symbols.

(defmulti collide (fn [a b] [(class a) (class b)]))
(defmethod collide [Asteroid  Spaceship] [_ _] "asteroid impacts hull")
(defmethod collide [Spaceship Spaceship] [_ _] "ships collide")

Python with the third-party multimethod package.

from multimethod import multimethod

@multimethod
def collide(a: Asteroid, b: Spaceship): return "asteroid impacts hull"
@multimethod
def collide(a: Spaceship, b: Spaceship): return "ships collide"

C# uses the dynamic keyword to defer overload resolution to run time, the idiomatic multimethod-like dispatch in C# 4.0 and later [14].

string Collide(Asteroid a, Spaceship b)  => "asteroid impacts hull";
string Collide(Spaceship a, Spaceship b) => "ships collide";

dynamic x = ..., y = ...;
var result = Collide(x, y);  // resolved at run time on actual types

Raku (Perl 6).

multi collide(Asteroid  $a, Spaceship $b) { "asteroid impacts hull" }
multi collide(Spaceship $a, Spaceship $b) { "ships collide" }

All forms compile to a runtime selection on the types of both arguments. They differ in syntax and in the dispatch machinery but share the same conceptual content.

ELI5: Multiple dispatch in one picture

Imagine a rule that says what happens when two things bump into each other. With single dispatch, only the first thing gets a say: the asteroid decides what "bump" means and then has to look at the other object to figure out the rest. With multiple dispatch, both things vote at the same time. "Asteroid plus spaceship" is one rule, "spaceship plus spaceship" is another rule, and the computer simply looks at the pair and runs the matching rule. Nobody is the boss; the combination chooses the behavior.

See also

References

  1. Wikipedia, "Multiple dispatch." https://en.wikipedia.org/wiki/Multiple_dispatch
  2. Bobrow, D. G., DeMichiel, L. G., Gabriel, R. P., Keene, S. E., Kiczales, G., & Moon, D. A. (1988). "Common Lisp Object System Specification." *SIGPLAN Notices* 23(SI), 1-142. https://dl.acm.org/doi/10.1145/885631.885633
  3. Shalit, A. (1996). *The Dylan Reference Manual*. Apple / Addison-Wesley. https://opendylan.org/books/drm/
  4. Bezanson, J., Edelman, A., Karpinski, S., & Shah, V. B. (2017). "Julia: A Fresh Approach to Numerical Computing." *SIAM Review* 59(1), 65-98. https://epubs.siam.org/doi/10.1137/141000671
  5. Karpinski, S. (2019). "The Unreasonable Effectiveness of Multiple Dispatch." JuliaCon 2019. https://www.youtube.com/watch?v=kc9HwsxE1OY
  6. Castagna, G. (1995). "Covariance and contravariance: conflict without a cause." *ACM TOPLAS* 17(3), 431-447. https://dl.acm.org/doi/10.1145/203095.203096
  7. Chambers, C. (1992). "Object-Oriented Multi-Methods in Cecil." *ECOOP '92*. https://link.springer.com/chapter/10.1007/BFb0053029
  8. Holst, K. M., & Bobrow, D. G. (1989). "Dispatch on Multiple Argument Types in Object-Oriented Programming." Xerox PARC technical report.
  9. Bobrow, D. G., Kahn, K., Kiczales, G., Masinter, L., Stefik, M., & Zdybel, F. (1986). "CommonLoops: Merging Lisp and Object-Oriented Programming." *OOPSLA '86*. https://dl.acm.org/doi/10.1145/28697.28700
  10. Wadler, P. (1998). "The Expression Problem." https://homepages.inf.ed.ac.uk/wadler/papers/expression/expression.txt
  11. Apple Computer (1992). *Dylan: An Object-Oriented Dynamic Language*. Apple Technical Publications.
  12. Raku Documentation, "Multi-dispatch." https://docs.raku.org/language/functions#Multi-dispatch
  13. Hickey, R. "Multimethods and Hierarchies." *Clojure documentation*. https://clojure.org/reference/multimethods
  14. Microsoft, "Using type dynamic." *C# Programming Guide*. https://learn.microsoft.com/en-us/dotnet/csharp/programming-guide/types/using-type-dynamic
  15. Sweeney, T. (2006). "The Next Mainstream Programming Language: A Game Developer's Perspective." POPL keynote. https://www.st.cs.uni-saarland.de/edu/seminare/2005/advanced-fp/docs/sweeny.pdf
  16. Keene, S. E. (1989). *Object-Oriented Programming in Common Lisp: A Programmer's Guide to CLOS*. Addison-Wesley.
  17. Cannon, H. I. (1982). "Flavors: A Non-Hierarchical Approach to Object-Oriented Programming." Symbolics technical report.
  18. ANSI INCITS 226-1994 (R2004), "American National Standard for Information Systems: Programming Language Common Lisp."
  19. Steele, G. L. (1990). *Common Lisp the Language*, 2nd edition. Digital Press. https://www.cs.cmu.edu/Groups/AI/html/cltl/cltl2.html
  20. Kiselyov, O. "Tiny CLOS for Scheme." https://okmij.org/ftp/Scheme/index.html#tiny-clos
  21. Ernst, M., Kaplan, C., & Chambers, C. (1998). "Predicate Dispatching: A Unified Theory of Dispatch." *ECOOP '98*. https://link.springer.com/chapter/10.1007/BFb0054099
  22. Julia Documentation, "Methods." https://docs.julialang.org/en/v1/manual/methods/
  23. MIT News (2018). "MIT-created programming language Julia 1.0 debuts." https://news.mit.edu/2018/mit-developed-julia-programming-language-debuts-juliacon-0827
  24. Coghlan, N. (2013). "PEP 443: Single-dispatch generic functions." Python Enhancement Proposals. https://peps.python.org/pep-0443/

Improve this article

Add missing citations, update stale details, or suggest a clearer explanation. Every suggestion is reviewed for sourcing before it goes live.

1 revision by 1 contributors · full history

Suggest edit