Recent from talks
Contribute something
Nothing was collected or created yet.
Multiple dispatch
View on Wikipedia| Polymorphism |
|---|
| Ad hoc polymorphism |
| Parametric polymorphism |
| Subtyping |
Multiple dispatch or multimethods is a feature of some programming languages in which a function or method can be dynamically dispatched based on the run-time (dynamic) type or, in the more general case, some other attribute of more than one of its arguments.[1] This is a generalization of single-dispatch polymorphism where a function or method call is dynamically dispatched based on the derived type of the object on which the method has been called. Multiple dispatch routes the dynamic dispatch to the implementing function or method using the combined characteristics of one or more arguments.
Understanding dispatch
[edit]Developers of computer software typically organize source code into named blocks variously called subroutines, procedures, subprograms, functions, or methods. The code in the function is executed by calling it – executing a piece of code that references its name. This transfers control temporarily to the called function; when the function's execution has completed, control is typically transferred back to the instruction in the caller that follows the reference.
Function names are usually selected so as to be descriptive of the function's purpose. It is sometimes desirable to give several functions the same name, often because they perform conceptually similar tasks, but operate on different types of input data. In such cases, the name reference at the function call site is not sufficient for identifying the block of code to be executed. Instead, the number and type of the arguments to the function call are also used to select among several function implementations.
In more conventional, i.e., single-dispatch object-oriented programming languages, when invoking a method (sending a message in Smalltalk, calling a member function in C++), one of its arguments is treated specially and used to determine which of the (potentially many) classes of methods of that name is to be applied. In many languages, the special argument is indicated syntactically; for example, a number of programming languages put the special argument before a dot in making a method call: special.method(other, arguments, here), so that lion.sound() would produce a roar, whereas sparrow.sound() would produce a chirp.
In contrast, in languages with multiple dispatch, the selected method is simply the one whose arguments match the number and type of the function call. There is no special argument that owns the function/method carried out in a particular call.
Multiple dispatch should be distinguished from function overloading, in which static typing information, such as a term's declared or inferred type (or base type in a language with subtyping) is used to determine which of several possibilities will be used at a given call site, and that determination is made at compile or link time (or some other time before program execution starts) and is thereafter invariant for a given deployment or run of the program. Many languages such as C++ offer robust function overloading but do not offer dynamic multiple dispatch (C++ only permits dynamic single dispatch through use of virtual functions).
Data types
[edit]When working with languages that can discriminate data types at compile time, selecting among the alternatives can occur then. The act of creating such alternative functions for compile time selection is usually referred to as overloading a function.
In programming languages that defer data type identification until run time (i.e., late binding), selection among alternative functions must occur then, based on the dynamically determined types of function arguments. Functions whose alternative implementations are selected in this manner are referred to most generally as multimethods.
There is some run-time cost associated with dynamically dispatching function calls. In some languages,[citation needed] the distinction between overloading and multimethods can be blurred, with the compiler determining whether compile time selection can be applied to a given function call, or whether slower run time dispatch is needed.
Issues
[edit]There are several known issues with dynamic-dispatch, both single and multiple. While many of these issues are solved for single-dispatch, which has been a standard feature in object-oriented programming languages for decades, these issues become more complicated in the multiple-dispatch case.
Expressiveness and modularity
[edit]In most popular programming languages, source code is delivered and deployed in granules of functionality which we will here call packages; actual terminology for this concept varies between language. Each package may contain multiple type, value, and function definitions, packages are often compiled separately in languages with a compilation step, and a non-cyclical dependency relationship may exist. A complete program is a set of packages, with a main package which may depend on several other packages, and the whole program consisting of the transitive closure of the dependency relationship.
The so-called expression problem relates to the ability for code in a depending package to extend behaviors (functions or datatypes) defined in a base package from within an including package, without modifying the source to the base package. Traditional single-dispatch OO languages make it trivial to add new datatypes but not new functions; traditional functional languages tend to have the opposite effect, and multiple dispatch, if implemented correctly, allows both. It is desirable for an implementation of multiple dispatch to have the following properties:
- It is possible to define different "cases" of a multi-method from within different packages without modifying the source of a base package.
- Inclusion of another package in the program should not change the behavior of a given multi-method call, when the call does not use any datatypes defined in the package.
- Conversely, if a datatype is defined in a given package, and a multi-method extension using that type is also defined in the same package, and a value of that type is passed (through a base type reference or into a generic function) into another package with no dependency on that package, and then the multi-method is invoked with that value as an argument, the multi-method case defined in the package which includes the type should be employed. To put it another way—within a given program, the same multi-method invoked with the same set of arguments should resolve to the same implementation, regardless of the location of the call site, and whether or not a given definition is "in scope" or "visible" at the point of the method call.
Ambiguity
[edit]It is generally desirable that for any given invocation of a multi-method, there be at most one "best" candidate among implementation cases of the multi-method, and/or that if there is not, that this be resolved in a predictable and deterministic fashion, including failure. Non-deterministic behavior is undesirable. Assuming a set of types with a non-circular subtyping relationship, one can define that one implementation of a multi-method is "better" (more specific) if all dynamically-dispatched arguments in the first are subtypes of all dynamically-dispatched arguments specified in the second, and at least one is a strict subtype. With single dispatch and in the absence of multiple inheritance, this condition is trivially satisfied, but with multiple dispatch, it is possible for two or more candidates to satisfy a given actual argument list, but neither is more specific than the other (one dynamic argument being the subtype in one case, another being the subtype in the other case). This particularly can happen if two different packages, neither depending on the other, both extend some multi-method with implementations concerning each package's types, and then a third package that includes both (possibly indirectly) then invokes the multi-method using arguments from both packages.
Possible resolutions include:
- Treating any ambiguous calls as an error. This might be caught at compile time (or otherwise before deployment), but might not be detected until runtime and produce a runtime error.
- Ordering the arguments, so e.g. the case with the most specific first argument is selected, and subsequent arguments are not considered for ambiguity resolution unless the first argument is insufficient to resolve the issue.
- Construction of other rules for resolving an ambiguity in one direction or another. Sometimes, such rules might be arbitrary and surprising. In the rules for static overload resolution in C++, for instance, a type which matches exactly is understandably considered a better match than a type which matches through a base type reference or a generic (template) parameter. However, if the only possible matches are either through a base type or a generic parameter, the generic parameter is preferred over the base type, a rule that sometimes produces surprising behavior.
Efficiency
[edit]Efficient implementation of single-dispatch, including in programming languages that are separately compiled to object code and linked with a low-level (not-language-aware) linker, including dynamically at program load/start time or even under the direction of the application code, are well known. The "vtable" method developed in C++ and other early OO languages (where each class has an array of function pointers corresponding to that class's virtual functions) is nearly as fast as a static method call, requiring O(1) overhead and only one additional memory lookup even in the un-optimized case. However, the vtable method uses the function name and not the argument type as its lookup key, and does not scale to the multiple dispatch case. (It also depends on the object-oriented paradigm of methods being features of classes, not standalone entities independent of any particular datatype).
Efficient implementation of multiple-dispatch remains an ongoing research problem.
Use in practice
[edit]To estimate how often multiple dispatch is used in practice, Muschevici et al.[2] studied programs that use dynamic dispatch. They analyzed nine applications, mostly compilers, written in six different languages: Common Lisp Object System, Dylan, Cecil, MultiJava, Diesel, and Nice. Their results show that 13–32% of generic functions use the dynamic type of one argument, while 2.7–6.5% of them use the dynamic type of multiple arguments. The remaining 65–93% of generic functions have one concrete method (overrider), and thus are not considered to use the dynamic types of their arguments. Further, the study reports that 2–20% of generic functions had two and 3–6% had three concrete function implementations. The numbers decrease rapidly for functions with more concrete overriders.
Multiple dispatch is used much more heavily in Julia, where multiple dispatch was a central design concept from the origin of the language: collecting the same statistics as Muschevici on the average number of methods per generic function, it was found that the Julia standard library uses more than double the amount of overloading than in the other languages analyzed by Muschevici, and more than 10 times in the case of binary operators.[3]
The data from these papers is summarized in the following table, where the dispatch ratio DR is the average number of methods per generic function; the choice ratio CR is the mean of the square of the number of methods (to better measure the frequency of functions with a large number of methods);[2][3] and the degree of specialization DoS is the average number of type-specialized arguments per method (i.e., the number of arguments that are dispatched on):
| Language | Average # methods (DR) | Choice ratio (CR) | Degree of specialization (DoS) |
|---|---|---|---|
| Cecil[2] | 2.33 | 63.30 | 1.06 |
| Common Lisp (CMU)[2] | 2.03 | 6.34 | 1.17 |
| Common Lisp (McCLIM)[2] | 2.32 | 15.43 | 1.17 |
| Common Lisp (Steel Bank)[2] | 2.37 | 26.57 | 1.11 |
| Diesel[2] | 2.07 | 31.65 | 0.71 |
| Dylan (Gwydion)[2] | 1.74 | 18.27 | 2.14 |
| Dylan (OpenDylan)[2] | 2.51 | 43.84 | 1.23 |
| Julia[3] | 5.86 | 51.44 | 1.54 |
| Julia (operators only)[3] | 28.13 | 78.06 | 2.01 |
| MultiJava[2] | 1.50 | 8.92 | 1.02 |
| Nice[2] | 1.36 | 3.46 | 0.33 |
Theory
[edit]The theory of multiple dispatching languages was first developed by Castagna et al., by defining a model for overloaded functions with late binding.[4][5] It yielded the first formalization of the problem of covariance and contravariance of object-oriented languages[6] and a solution to the problem of binary methods.[7]
Examples
[edit]Distinguishing multiple and single dispatch may be made clearer by an example. Imagine a game that has, among its (user-visible) objects, spaceships and asteroids. When two objects collide, the program may need to do different things according to what has just hit what.
Languages with built-in multiple dispatch
[edit]C#
[edit]C# introduced support for dynamic multimethods in version 4[8] (April 2010) using the 'dynamic' keyword. The following example demonstrates multimethods. Like many other statically-typed languages, C# also supports static method overloading.[9] Microsoft expects that developers will choose static typing over dynamic typing in most scenarios.[10] The 'dynamic' keyword supports interoperability with COM objects and dynamically-typed .NET languages.
The example below uses features introduced in C# 9 and C# 10.
using static ColliderLibrary;
Console.WriteLine(Collide(new Asteroid(101), new Spaceship(300)));
Console.WriteLine(Collide(new Asteroid(10), new Spaceship(10)));
Console.WriteLine(Collide(new Spaceship(101), new Spaceship(10)));
string Collide(SpaceObject x, SpaceObject y) =>
x.Size > 100 && y.Size > 100
? "Big boom!"
: CollideWith(x as dynamic, y as dynamic); // Dynamic dispatch to CollideWith method
class ColliderLibrary
{
public static string CollideWith(Asteroid x, Asteroid y) => "a/a";
public static string CollideWith(Asteroid x, Spaceship y) => "a/s";
public static string CollideWith(Spaceship x, Asteroid y) => "s/a";
public static string CollideWith(Spaceship x, Spaceship y) => "s/s";
}
abstract record SpaceObject(int Size);
record Asteroid(int Size) : SpaceObject(Size);
record Spaceship(int Size) : SpaceObject(Size);
Output:
Big boom!
a/s
s/s
Groovy
[edit]Groovy is a general purpose Java compatible/interusable JVM language, which, contrary to Java, uses late binding / multiple dispatch.[11]
/*
Groovy implementation of C# example above
Late binding works the same when using non-static methods or compiling class/methods statically
(@CompileStatic annotation)
*/
class Program {
static void main(String[] args) {
println Collider.collide(new Asteroid(101), new Spaceship(300))
println Collider.collide(new Asteroid(10), new Spaceship(10))
println Collider.collide(new Spaceship(101), new Spaceship(10))
}
}
class Collider {
static String collide(SpaceObject x, SpaceObject y) {
(x.size > 100 && y.size > 100) ? "big-boom" : collideWith(x, y) // Dynamic dispatch to collideWith method
}
private static String collideWith(Asteroid x, Asteroid y) { "a/a" }
private static String collideWith(Asteroid x, Spaceship y) { "a/s" }
private static String collideWith(Spaceship x, Asteroid y) { "s/a" }
private static String collideWith(Spaceship x, Spaceship y) { "s/s"}
}
class SpaceObject {
int size
SpaceObject(int size) { this.size = size }
}
@InheritConstructors class Asteroid extends SpaceObject {}
@InheritConstructors class Spaceship extends SpaceObject {}
Common Lisp
[edit]In a language with multiple dispatch, such as Common Lisp, it might look more like this (Common Lisp example shown):
(defclass asteroid () ((size :reader size :initarg :size)))
(defclass spaceship () ((size :reader size :initarg :size)))
(defun space-object (class size) (make-instance class :size size))
; collide-with is a generic function with multiple dispatch
(defmethod collide-with ((x asteroid) (y asteroid)) "a/a")
(defmethod collide-with ((x asteroid) (y spaceship)) "a/s")
(defmethod collide-with ((x spaceship) (y asteroid)) "s/a")
(defmethod collide-with ((x spaceship) (y spaceship)) "s/s")
(defun collide (x y)
(if (and (> (size x) 100) (> (size y) 100))
"big-boom"
(collide-with x y)))
(print (collide (space-object 'asteroid 101) (space-object 'spaceship 300)))
(print (collide (space-object 'asteroid 10) (space-object 'spaceship 10)))
(print (collide (space-object 'spaceship 101) (space-object 'spaceship 10)))
and similarly for the other methods. Explicit testing and "dynamic casting" are not used.
In the presence of multiple dispatch, the traditional idea of methods as being defined in classes and contained in objects becomes less appealing—each collide-with method above is attached to two different classes, not one. Hence, the special syntax for method invocation generally disappears, so that method invocation looks exactly like ordinary function invocation, and methods are grouped not in classes but in generic functions.
Julia
[edit]Julia has built-in multiple dispatch, and it is central to the language design.[3] The Julia version of the example above might look like:
abstract type SpaceObject end
struct Asteroid <: SpaceObject
size::Int
end
struct Spaceship <: SpaceObject
size::Int
end
collide_with(::Asteroid, ::Spaceship) = "a/s"
collide_with(::Spaceship, ::Asteroid) = "s/a"
collide_with(::Spaceship, ::Spaceship) = "s/s"
collide_with(::Asteroid, ::Asteroid) = "a/a"
collide(x::SpaceObject, y::SpaceObject) = (x.size > 100 && y.size > 100) ? "Big boom!" : collide_with(x, y)
Output:
julia> collide(Asteroid(101), Spaceship(300))
"Big boom!"
julia> collide(Asteroid(10), Spaceship(10))
"a/s"
julia> collide(Spaceship(101), Spaceship(10))
"s/s"
Raku
[edit]Raku, like Perl, uses proven ideas from other languages, and type systems have shown themselves to offer compelling advantages in compiler-side code analysis and powerful user-side semantics via multiple dispatch.
It has both multimethods, and multisubs. Since most operators are subroutines, it also has multiple dispatched operators.
Along with the usual type constraints, it also has where constraints that allow making very specialized subroutines.
subset Mass of Real where 0 ^..^ Inf;
role Stellar-Object {
has Mass $.mass is required;
method name () returns Str {...};
}
class Asteroid does Stellar-Object {
method name () { 'an asteroid' }
}
class Spaceship does Stellar-Object {
has Str $.name = 'some unnamed spaceship';
}
my Str @destroyed = < obliterated destroyed mangled >;
my Str @damaged = « damaged 'collided with' 'was damaged by' »;
# We add multi candidates to the numeric comparison operators because we are comparing them numerically,
# but makes no sense to have the objects coerce to a Numeric type.
# ( If they did coerce we wouldn't necessarily need to add these operators. )
# We could have also defined entirely new operators this same way.
multi sub infix:« <=> » ( Stellar-Object:D $a, Stellar-Object:D $b ) { $a.mass <=> $b.mass }
multi sub infix:« < » ( Stellar-Object:D $a, Stellar-Object:D $b ) { $a.mass < $b.mass }
multi sub infix:« > » ( Stellar-Object:D $a, Stellar-Object:D $b ) { $a.mass > $b.mass }
multi sub infix:« == » ( Stellar-Object:D $a, Stellar-Object:D $b ) { $a.mass == $b.mass }
# Define a new multi dispatcher, and add some type constraints to the parameters.
# If we didn't define it we would have gotten a generic one that didn't have constraints.
proto sub collide ( Stellar-Object:D $, Stellar-Object:D $ ) {*}
# No need to repeat the types here since they are the same as the prototype.
# The 'where' constraint technically only applies to $b not the whole signature.
# Note that the 'where' constraint uses the `<` operator candidate we added earlier.
multi sub collide ( $a, $b where $a < $b ) {
say "$a.name() was @destroyed.pick() by $b.name()";
}
multi sub collide ( $a, $b where $a > $b ) {
# redispatch to the previous candidate with the arguments swapped
samewith $b, $a;
}
# This has to be after the first two because the other ones
# have 'where' constraints, which get checked in the
# order the subs were written. ( This one would always match. )
multi sub collide ( $a, $b ) {
# randomize the order
my ($n1, $n2) = ( $a.name, $b.name ).pick(*);
say "$n1 @damaged.pick() $n2";
}
# The following two candidates can be anywhere after the proto,
# because they have more specialized types than the preceding three.
# If the ships have unequal mass one of the first two candidates gets called instead.
multi sub collide ( Spaceship $a, Spaceship $b where $a == $b ){
my ($n1, $n2) = ( $a.name, $b.name ).pick(*);
say "$n1 collided with $n2, and both ships were ",
( @destroyed.pick, 'left damaged' ).pick;
}
# You can unpack the attributes into variables within the signature.
# You could even have a constraint on them `(:mass($a) where 10)`.
multi sub collide ( Asteroid $ (:mass($a)), Asteroid $ (:mass($b)) ){
say "two asteroids collided and combined into one larger asteroid of mass { $a + $b }";
}
my Spaceship $Enterprise .= new(:mass(1),:name('The Enterprise'));
collide Asteroid.new(:mass(.1)), $Enterprise;
collide $Enterprise, Spaceship.new(:mass(.1));
collide $Enterprise, Asteroid.new(:mass(1));
collide $Enterprise, Spaceship.new(:mass(1));
collide Asteroid.new(:mass(10)), Asteroid.new(:mass(5));
Extending languages with multiple-dispatch libraries
[edit]JavaScript
[edit]In languages that do not support multiple dispatch at the language definition or syntactic level, it is often possible to add multiple dispatch using a library extension. JavaScript and TypeScript do not support multimethods at the syntax level, but it is possible to add multiple dispatch via a library. For example, the multimethod package[12] provides an implementation of multiple dispatch, generic functions.
Dynamically-typed version in JavaScript:
import { multi, method } from '@arrows/multimethod'
class Asteroid {}
class Spaceship {}
const collideWith = multi(
method([Asteroid, Asteroid], (x, y) => {
// deal with asteroid hitting asteroid
}),
method([Asteroid, Spaceship], (x, y) => {
// deal with asteroid hitting spaceship
}),
method([Spaceship, Asteroid], (x, y) => {
// deal with spaceship hitting asteroid
}),
method([Spaceship, Spaceship], (x, y) => {
// deal with spaceship hitting spaceship
}),
)
Statically-typed version in TypeScript:
import { multi, method, Multi } from '@arrows/multimethod'
class Asteroid {}
class Spaceship {}
type CollideWith = Multi & {
(x: Asteroid, y: Asteroid): void
(x: Asteroid, y: Spaceship): void
(x: Spaceship, y: Asteroid): void
(x: Spaceship, y: Spaceship): void
}
const collideWith: CollideWith = multi(
method([Asteroid, Asteroid], (x, y) => {
// deal with asteroid hitting asteroid
}),
method([Asteroid, Spaceship], (x, y) => {
// deal with asteroid hitting spaceship
}),
method([Spaceship, Asteroid], (x, y) => {
// deal with spaceship hitting asteroid
}),
method([Spaceship, Spaceship], (x, y) => {
// deal with spaceship hitting spaceship
}),
)
Python
[edit]Multiple dispatch can be added to Python using a library extension. For example, using the module multimethod.py[13] and also with the module multimethods.py[14] which provides CLOS-style multimethods for Python without changing the underlying syntax or keywords of the language.
from typing import Any
import game_behaviors
from game_objects import Asteroid, Spaceship
from multimethods import Dispatch
collide: Dispatch = Dispatch()
collide.add_rule((Asteroid, Spaceship), game_behaviors.as_func)
collide.add_rule((Spaceship, Spaceship), game_behaviors.ss_func)
collide.add_rule((Spaceship, Asteroid), game_behaviors.sa_func)
def aa_func(a: Any, b: Any) -> None:
"""Behavior when asteroid hits asteroid."""
# ...define new behavior...
collide.add_rule((Asteroid, Asteroid), aa_func)
# ...later...
collide(thing1, thing2)
Functionally, this is very similar to the CLOS example, but the syntax is conventional Python.
Using decorators (introduced since Python 2.4), Guido van Rossum produced a sample implementation of multimethods[15] with a simplified syntax:
@multimethod(Asteroid, Asteroid)
def collide(a: Asteroid, b: Asteroid) -> None:
"""Behavior when asteroid hits a asteroid."""
# ...define new behavior...
@multimethod(Asteroid, Spaceship)
def collide(a: Asteroid, b: Spaceship) -> None:
"""Behavior when asteroid hits a spaceship."""
# ...define new behavior...
# ... define other multimethod rules ...
and then it goes on to define the multimethod decorator.
The PEAK-Rules package provides multiple dispatch with a syntax similar to the above example.[16] It was later replaced by PyProtocols.[17]
The Reg library also supports multiple and predicate dispatch.[18]
With the introduction of type hints, multiple dispatch is possible with even simpler syntax. For example, using plum-dispatch,
from plum import dispatch
@dispatch
def collide(a: Asteroid, b: Asteroid) -> None:
"""Behavior when asteroid hits a asteroid."""
# ...define new behavior...
@dispatch
def collide(a: Asteroid, b: Spaceship) -> None:
"""Behavior when asteroid hits a spaceship."""
# ...define new behavior...
# ...define further rules...
Emulating multiple dispatch
[edit]C
[edit]C does not have dynamic dispatch, so it must be implemented manually in some form. Often an enum is used to identify the subtype of an object. Dynamic dispatch can be done by looking up this value in a function pointer branch table. Here is a simple example in C:
typedef void (*CollisionCase)(void);
void collisionAsteroidAsteroid(void) {
// handle Asteroid-Asteroid collision...
}
void collisionAsteroidSpaceship(void) {
// handle Asteroid-Spaceship collision...
}
void collisionSpaceshipAsteroid(void) {
// handle Spaceship-Asteroid collision...
}
void collisionSpaceshipSpaceship(void) {
// handle Spaceship-Spaceship collision...
}
typedef enum {
COLLIDEABLE_ASTEROID = 0,
COLLIDEABLE_SPACESHIP,
COLLIDEABLE_COUNT // not a type of Collideable itself, instead used to find number of space objects defined
} Collideable;
CollisionCase collisionCases[COLLIDEABLE_COUNT][COLLIDEABLE_COUNT] = {
{&collisionAsteroidAsteroid, &collisionAsteroidSpaceship},
{&collisionSpaceshipAsteroid, &collisionSpaceshipSpaceship}
};
void collide(Collideable a, Collideable b) {
(*collisionCases[a][b])();
}
int main(void) {
collide(COLLIDEABLE_SPACESHIP, COLLIDEABLE_ASTEROID);
}
With the C Object System library,[19] C does support dynamic dispatch similar to CLOS. It is fully extensible and does not need any manual handling of the methods. Dynamic message (methods) are dispatched by the dispatcher of COS, which is faster than Objective-C. Here is an example in COS:
#include <stdio.h>
#include <cos/Object.h>
#include <cos/gen/object.h>
// classes
defclass (Asteroid)
// data members
endclass
defclass (Spaceship)
// data members
endclass
// generics
defgeneric (_Bool, collide_with, _1, _2);
// multimethods
defmethod (_Bool, collide_with, Asteroid, Asteroid)
// deal with asteroid hitting asteroid
endmethod
defmethod (_Bool, collide_with, Asteroid, Spaceship)
// deal with asteroid hitting spaceship
endmethod
defmethod (_Bool, collide_with, Spaceship, Asteroid)
// deal with spaceship hitting asteroid
endmethod
defmethod (_Bool, collide_with, Spaceship, Spaceship)
// deal with spaceship hitting spaceship
endmethod
// example of use
int main(void)
{
OBJ a = gnew(Asteroid);
OBJ s = gnew(Spaceship);
printf("<a,a> = %d\n", collide_with(a, a));
printf("<a,s> = %d\n", collide_with(a, s));
printf("<s,a> = %d\n", collide_with(s, a));
printf("<s,s> = %d\n", collide_with(s, s));
grelease(a);
grelease(s);
}
C++
[edit]As of 2021[update], C++ natively supports only single dispatch, though adding multi-methods (multiple dispatch) was proposed by Bjarne Stroustrup (and collaborators) in 2007.[20] The methods of working around this limit are analogous: use either the visitor pattern, dynamic cast or a library:
// Example using run time type comparison via dynamic_cast
class Collideable {
public:
virtual void collideWith(Collideable& other) = 0;
};
class Asteroid: public Collideable {
public:
void collideWith(Collideable& other) {
// dynamic_cast to a pointer type returns nullptr if the cast fails
// (dynamic_cast to a reference type would throw an exception on failure)
if (Asteroid* asteroid = dynamic_cast<Asteroid*>(&other)) {
// handle Asteroid-Asteroid collision
} else if (Spaceship* spaceship = dynamic_cast<Spaceship*>(&other)) {
// handle Asteroid-Spaceship collision
} else {
// default collision handling here
}
}
};
class Spaceship: public Collideable {
public:
void collideWith(Collideable& other) {
if (Asteroid* asteroid = dynamic_cast<Asteroid*>(&other)) {
// handle Spaceship-Asteroid collision
} else if (Spaceship* spaceship = dynamic_cast<Spaceship*>(&other)) {
// handle Spaceship-Spaceship collision
} else {
// default collision handling here
}
}
};
or pointer-to-method lookup table:
import std;
using std::unordered_map;
class Collideable {
protected:
explicit Collideable(uint32_t cid):
tid{cid} {}
virtual ~Collideable() = default;
const uint32_t tid; // type id
using CollisionHandler = void (Collideable::*)(Collideable& other);
using CollisionHandlerMap = unordered_map<uint64_t, CollisionHandler>;
static void addHandler(uint32_t id1, uint32_t id2, CollisionHandler handler) {
collisionCases.insert(CollisionHandlerMap::value_type(key(id1, id2), handler));
}
static uint64_t key(uint32_t id1, uint32_t id2) {
return uint64_t(id1) << 32 | id2;
}
static CollisionHandlerMap collisionCases;
public:
void collideWith(Collideable& other) {
auto handler = collisionCases.find(key(tid, other.tid));
if (handler != collisionCases.end()) {
(this->*handler->second)(other); // pointer-to-method call
} else {
// default collision handling
}
}
};
class Asteroid: public Collideable {
private:
void asteroidCollision(Collideable& other) {
// handle Asteroid-Asteroid collision
}
void spaceshipCollision(Collideable& other) {
// handle Asteroid-Spaceship collision
}
public:
Asteroid():
Collideable(cid) {}
~Asteroid() = default;
static void initCases();
static const uint32_t cid;
};
class Spaceship: public Collideable {
private:
void asteroidCollision(Collideable& other) {
// handle Spaceship-Asteroid collision
}
void spaceshipCollision(Collideable& other) {
// handle Spaceship-Spaceship collision
}
public:
Spaceship():
Collideable(cid) {}
~Spaceship() = default;
static void initCases();
static const uint32_t cid; // class id
};
Collideable::CollisionHandlerMap Collideable::collisionCases;
const uint32_t Asteroid::cid = typeid(Asteroid).hash_code();
const uint32_t Spaceship::cid = typeid(Spaceship).hash_code();
void Asteroid::initCases() {
addHandler(cid, cid, CollisionHandler(&Asteroid::asteroidCollision));
addHandler(cid, Spaceship::cid, CollisionHandler(&Asteroid::spaceshipCollision));
}
void Spaceship::initCases() {
addHandler(cid, Asteroid::cid, CollisionHandler(&Spaceship::asteroidCollision));
addHandler(cid, cid, CollisionHandler(&Spaceship::spaceshipCollision));
}
int main(int argc, char* argv[]) {
Asteroid::initCases();
Spaceship::initCases();
Asteroid a1;
Asteroid a2;
Spaceship s1;
Spaceship s2;
a1.collideWith(a2);
a1.collideWith(s1);
s1.collideWith(s2);
s1.collideWith(a1);
}
The YOMM2 library[21] provides a fast, orthogonal implementation of open multimethods.
The syntax for declaring open methods is inspired by a proposal for a native C++ implementation. The library requires that the user registers all the classes used as virtual arguments (and their sub-classes), but does not require any modifications to existing code. Methods are implemented as ordinary inline C++ functions; they can be overloaded and they can be passed by pointer. There is no limit on the number of virtual arguments, and they can be arbitrarily mixed with non-virtual arguments.
The library uses a combination of techniques (compressed dispatch tables, collision free integer hash table) to implement method calls in constant time, while mitigating memory usage. Dispatching a call to an open method with a single virtual argument takes only 15–30% more time than calling an ordinary virtual member function, when a modern optimizing compiler is used.
The Asteroids example can be implemented as follows:
#include <yorel/yomm2/keywords.hpp>
import std;
using std::unique_ptr;
class Collideable {
public:
virtual ~Collideable() = default;
};
class Asteroid : public Collideable {
// ...
};
class Spaceship : public Collideable {
// ...
};
register_classes(Collideable, Spaceship, Asteroid);
declare_method(void, collideWith, (virtual_<Collideable&>, virtual_<Collideable&>));
define_method(void, collideWith, (Collideable& left, Collideable& right)) {
// default collision handling
}
define_method(void, collideWith, (Asteroid& left, Asteroid& right)) {
// handle Asteroid-Asteroid collision
}
define_method(void, collideWith, (Asteroid& left, Spaceship& right)) {
// handle Asteroid-Spaceship collision
}
define_method(void, collideWith, (Spaceship& left, Asteroid& right)) {
// handle Spaceship-Asteroid collision
}
define_method(void, collideWith, (Spaceship& left, Spaceship& right)) {
// handle Spaceship-Spaceship collision
}
int main(int argc, char* argv[]) {
yorel::yomm2::update_methods();
unique_ptr<Collideable> a1(std::make_unique<Asteroid>());
unique_ptr<Collideable> a2(std::make_unique<Asteroid>());
unique_ptr<Collideable> s1(std::make_unique<Spaceship>());
unique_ptr<Collideable> s2(std::make_unique<Spaceship>());
// note: types partially erased
collideWith(*a1, *a2); // Asteroid-Asteroid collision
collideWith(*a1, *s1); // Asteroid-Spaceship collision
collideWith(*s1, *a1); // Spaceship-Asteroid collision
collideWith(*s1, *s2); // Spaceship-Spaceship collision
return 0;
}
Stroustrup mentions in The Design and Evolution of C++ that he liked the concept of multimethods and considered implementing it in C++ but claims to have been unable to find an efficient sample implementation (comparable to virtual functions) and resolve some possible type ambiguity problems. He then states that although the feature would still be nice to have, that it can be approximately implemented using double dispatch or a type based lookup table as outlined in the C/C++ example above so is a low priority feature for future language revisions.[22]
D
[edit]As of 2021[update], as do many other object-oriented programming languages, D natively supports only single dispatch. However, it is possible to emulate open multimethods as a library function in D. The openmethods library[23] is an example.
// Declaration
Matrix plus(virtual!Matrix, virtual!Matrix);
// The override for two DenseMatrix objects
@method
Matrix _plus(DenseMatrix a, DenseMatrix b)
{
const int nr = a.rows;
const int nc = a.cols;
assert(a.nr == b.nr);
assert(a.nc == b.nc);
auto result = new DenseMatrix;
result.nr = nr;
result.nc = nc;
result.elems.length = a.elems.length;
result.elems[] = a.elems[] + b.elems[];
return result;
}
// The override for two DiagonalMatrix objects
@method
Matrix _plus(DiagonalMatrix a, DiagonalMatrix b)
{
assert(a.rows == b.rows);
double[] sum;
sum.length = a.elems.length;
sum[] = a.elems[] + b.elems[];
return new DiagonalMatrix(sum);
}
Java
[edit]In a language with only single dispatch, such as Java, multiple dispatch can be emulated with multiple levels of single dispatch:
interface Collideable {
void collideWith(final Collideable other);
/* These methods would need different names in a language without method overloading. */
void collideWith(final Asteroid asteroid);
void collideWith(final Spaceship spaceship);
}
class Asteroid implements Collideable {
public void collideWith(final Collideable other) {
// Call collideWith on the other object.
other.collideWith(this);
}
public void collideWith(final Asteroid asteroid) {
// Handle Asteroid-Asteroid collision.
}
public void collideWith(final Spaceship spaceship) {
// Handle Asteroid-Spaceship collision.
}
}
class Spaceship implements Collideable {
public void collideWith(final Collideable other) {
// Call collideWith on the other object.
other.collideWith(this);
}
public void collideWith(final Asteroid asteroid) {
// Handle Spaceship-Asteroid collision.
}
public void collideWith(final Spaceship spaceship) {
// Handle Spaceship-Spaceship collision.
}
}
Run time instanceof checks at one or both levels can also be used.
Support in programming languages
[edit]Primary paradigm
[edit]Supporting general multimethods
[edit]- C# 4.0[25]
- Cecil[26]
- Clojure[27]
- Common Lisp (via the Common Lisp Object System)[28]
- Dylan[29]
- Emacs Lisp (via cl-defmethod)
- Fortress[30]
- Groovy[31]
- Lasso[32][33]
- Nim, up to v0.19.x (from v0.20.0 it is necessary to pass a compiler flag)[34]
- Raku[35]
- R[36]
- TADS[37]
- Visual Basic (.NET) (VB.NET)[38] via late binding, also via .Net DLR[39]
- Wolfram Language[40] via symbolic pattern matching
- Xtend[41]
Via extensions
[edit]- Any .NET framework language (via the library MultiMethods.NET)
- C (via the library C Object System)
- C# (via the library multimethod-sharp)
- C++ (via the library yomm2, multimethods and omm)
- D (via the library openmethods)
- Factor (via the standard multimethods vocabulary)
- Java (using the extension MultiJava)
- JavaScript (via package @arrows/multimethod)
- Perl (via the module Class::Multimethods)
- Python (via PEAK-Rules, RuleDispatch, gnosis.magic.multimethods, PyMultimethods, multipledispatch, or plum-dispatch)
- Racket (via multimethod-lib)
- Ruby (via the library The Multiple Dispatch Library and Multimethod Package and Vlx-Multimethods Package)
- Scheme (via e.g. TinyCLOS)
- TypeScript (via package @arrows/multimethod)
See also
[edit]References
[edit]- ^ Ranka, Sanjay; Banerjee, Arunava; Biswas, Kanad Kishore; Dua, Sumeet; Mishra, Prabhat; Moona, Rajat (2010-07-26). Contemporary Computing: Second International Conference, IC3 2010, Noida, India, August 9–11, 2010. Proceedings. Springer. ISBN 9783642148248.
- ^ a b c d e f g h i j k Muschevici, Radu; Potanin, Alex; Tempero, Ewan; Noble, James (2008). "Multiple dispatch in practice". Proceedings of the 23rd ACM SIGPLAN conference on Object-oriented programming systems languages and applications. OOPSLA '08. Nashville, TN, USA: ACM. pp. 563–582. doi:10.1145/1449764.1449808. ISBN 9781605582153. S2CID 7605233.
- ^ a b c d e Bezanson, Jeff; Edelman, Alan; Karpinski, Stefan; Shah, Viral B. (7 February 2017). "Julia: A fresh approach to numerical computing". SIAM Review. 59 (1): 65–98. arXiv:1411.1607. doi:10.1137/141000671. S2CID 13026838.
- ^ Castagna, Giuseppe; Ghelli, Giorgio & Longo, Giuseppe (1995). "A calculus for overloaded functions with subtyping". Information and Computation. 117 (1): 115–135. doi:10.1006/inco.1995.1033.
- ^ Castagna, Giuseppe (1996). Object-Oriented Programming: A Unified Foundation. Progress in Theoretical Computer Science. Birkhäuser. p. 384. ISBN 978-0-8176-3905-1.
- ^ Castagna, Giuseppe (1995). "Covariance and contravariance: conflict without a cause". ACM Transactions on Programming Languages and Systems. 17 (3): 431–447. CiteSeerX 10.1.1.115.5992. doi:10.1145/203095.203096. S2CID 15402223.
- ^ Bruce, Kim; Cardelli, Luca; Castagna, Giuseppe; Leavens, Gary T.; Pierce, Benjamin (1995). "On binary methods". Theory and Practice of Object Systems. 1 (3): 221–242. doi:10.1002/j.1096-9942.1995.tb00019.x. Retrieved 2013-04-19.
- ^ "Using type dynamic (C# Programming Guide)". Retrieved 2020-05-14.
- ^ "Basic concepts". Retrieved 2020-05-14.
- ^ "Dynamic .NET - Understanding the Dynamic Keyword in C# 4". 10 August 2015. Retrieved 2020-05-14.
- ^ Groovy - Multi-methods
- ^ @arrows/multimethod Multiple dispatch in JavaScript/TypeScript with configurable dispatch resolution by Maciej Cąderek.
- ^ Coady, Aric, multimethod: Multiple argument dispatching., retrieved 2021-01-28
- ^ multimethods.py Archived 2005-03-09 at the Wayback Machine, Multiple dispatch in Python with configurable dispatch resolution by David Mertz, et al.
- ^ "Five-minute Multimethods in Python".
- ^ "PEAK-Rules 0.5a1.dev". Python Package Index. Retrieved 21 March 2014.
- ^ "PyProtocols". Python Enterprise Application Kit. Retrieved 26 April 2019.
- ^ "Reg". Read the docs. Retrieved 26 April 2019.
- ^ "C Object System: A framework that brings C to the level of other high level programming languages and beyond: CObjectSystem/COS". GitHub. 2019-02-19.
- ^ "Report on language support for Multi-Methods and Open-Methods for C ++" (PDF). 2007-03-11.
Multiple dispatch – the selection of a function to be invoked based on the dynamic type of two or more arguments – is a solution to several classical problems in object-oriented programming.
- ^ yomm2, Fast, Orthogonal Open Multi-Methods for C++ by Jean-Louis Leroy.
- ^ Stroustrup, Bjarne (1994). "Section 13.8". The Design and Evolution of C++. Indianapolis, IN, U.S.A: Addison Wesley. Bibcode:1994dec..book.....S. ISBN 978-0-201-54330-8.
- ^ openmethods, Open Multi-Methods for D by Jean-Louis Leroy.
- ^ "Methods". The Julia Manual. Julialang. Archived from the original on 17 July 2016. Retrieved 11 May 2014.
- ^ "Multimethods in C# 4.0 With 'Dynamic'". Retrieved 2009-08-20.
- ^ "Cecil Language". Retrieved 2008-04-13.
- ^ "Multimethods in Clojure". Retrieved 2008-09-04.
- ^ Steele, Guy L. (1990). "28". Common LISP: The Language. Bedford, MA, U.S.A: Digital Press. ISBN 978-1-55558-041-4.
- ^ "Background and Goals". Retrieved 2008-04-13.
- ^ "The Fortress Language Specification, Version 1.0" (PDF). Archived from the original (PDF) on 2013-01-20. Retrieved 2010-04-23.
- ^ "Multimethods in Groovy". Retrieved 2008-04-13.
- ^ "Methods – LassoGuide 9.2". Retrieved 2014-11-11.
- ^ "Visitor Pattern Versus Multimethods". Retrieved 2008-04-13.
- ^ "Nim Manual: Multi-methods". Retrieved 2022-05-03.
- ^ "Perl 6 FAQ". Retrieved 2008-04-13.
- ^ "How S4 Methods Work" (PDF). Retrieved 2008-04-13.
- ^ "TADS 3 System Manual". Retrieved 2012-03-19.
- ^ "VB.Net Multiple Dispatch". Retrieved 2020-03-31.
- ^ "New Features in C#4.0 and VB.Net 10.0". 4 November 2010. Retrieved 2020-03-31.
- ^ "Notes for Programming Language Experts". Retrieved 2016-08-21.
- ^ "Multiple dispatch".
External links
[edit]- Stroustrup, Bjarne; Solodkyy, Yuriy; Pirkelbauer, Peter (2007). Open Multi-Methods for C++ (PDF). ACM 6th International Conference on Generative Programming and Component Engineering.
- "Dynamic multiple dispatch". docs.racket-lang.org. Retrieved 2018-03-12.
Multiple dispatch
View on GrokipediaCore Concepts
Definition and Basics
Multiple dispatch is a polymorphic mechanism in programming languages that selects the appropriate method implementation at runtime based on the types of two or more arguments to a function or method call.[2] This enables more flexible and generic programming by allowing behavior to depend on the combined types of multiple inputs, extending beyond the limitations of single-argument polymorphism where dispatch occurs solely on the receiver or first argument.[2] The concept was pioneered in the 1980s through systems like CommonLoops and was standardized in the late 1980s with the Common Lisp Object System (CLOS), which introduced multiple dispatch as part of its generic function framework.[4] It built directly on earlier work in CommonLoops, an extension to Common Lisp developed in 1986 that introduced multimethods for dynamic dispatch on multiple arguments to integrate object-oriented features with Lisp's procedural style.[5] These developments drew from polymorphic ideas in 1970s Lisp-based object systems like Flavors, though multimethods specifically emerged in the 1980s to address limitations in single-dispatch mechanisms.[6] A key distinction from method overloading is that overloading resolves function calls at compile time based on static parameter signatures, often limited to the number or types of arguments without runtime type combination sensitivity.[2] In contrast, multiple dispatch performs resolution dynamically at runtime, considering the actual types of multiple arguments to select the most specific method, which supports greater expressiveness in handling type interactions.[2] The basic workflow of multiple dispatch involves forming a signature from the runtime types of the arguments, which is then matched against a method table containing specialized methods defined for various type combinations.[2] The system selects the most applicable method—typically the most specific one that matches—using a dispatch resolution process, ensuring the chosen implementation aligns with the actual argument types encountered during execution.[2]Single vs. Multiple Dispatch
Single dispatch is a core mechanism in many object-oriented programming languages, such as Java and C++, where the selection of a method to execute at runtime depends exclusively on the dynamic type of the receiver object, which is typically the first argument in the method call.[7] This process is commonly implemented using virtual method tables (vtables), which are data structures associated with each class containing pointers to the appropriate method implementations; when a virtual method is invoked, the runtime system consults the vtable of the receiver's actual type to resolve and execute the correct function.[8][9] As a result, polymorphism is confined to the receiver, while subsequent arguments are handled through static type checking or overloading, limiting the flexibility for operations that depend on multiple dynamic types. One key limitation of single dispatch arises when operations require behavior that varies based on the types of more than one argument, such as in interactions between heterogeneous objects. In these cases, programmers often resort to design patterns like the visitor pattern, which simulates multi-argument dispatch through a series of single dispatches (e.g., double dispatch) but introduces complexity, breaks encapsulation, and requires modifications to existing class hierarchies.[7] For instance, implementing a comparison or rendering operation between objects of different subtypes (e.g., 2D and 3D points) under single dispatch ignores the dynamic type of the second argument, leading to incorrect or inefficient behavior that must be manually overridden.[10] Multiple dispatch extends this model by resolving method selection based on the dynamic types of all arguments in the call, rather than just the receiver, thereby allowing for more expressive and natural handling of multi-type interactions.[7] This enables symmetric treatment of arguments, where the dispatch rules apply uniformly without privileging the first argument, and facilitates the definition of generic functions whose behavior adapts to any combination of argument types.[11] As a result, patterns like visitors become unnecessary, as the language directly supports operations that depend on multiple types without auxiliary mechanisms. To illustrate the difference, consider a simple drawing operation that renders a shape with a specific color. Under single dispatch, the method is attached to the shape object, and color handling is limited to static overloading, potentially requiring awkward type checks or separate methods for each color type:// Single dispatch example ([pseudocode](/page/Pseudocode), inspired by [Java](/page/Java)/C++ style)
class [Shape](/page/Shape) {
virtual void draw([Color](/page/Color) c) {
// Default drawing ignores specific color types
renderBasic(c.getRGB());
}
}
class Circle extends Shape {
void draw(Color c) {
if (c instanceof RedColor) {
renderCircleRed(); // Manual type check needed
} else {
renderBasic(c.getRGB());
}
}
}
// Usage: circle.draw(redColor); // Dispatches only on Circle, checks color manually
// Single dispatch example ([pseudocode](/page/Pseudocode), inspired by [Java](/page/Java)/C++ style)
class [Shape](/page/Shape) {
virtual void draw([Color](/page/Color) c) {
// Default drawing ignores specific color types
renderBasic(c.getRGB());
}
}
class Circle extends Shape {
void draw(Color c) {
if (c instanceof RedColor) {
renderCircleRed(); // Manual type check needed
} else {
renderBasic(c.getRGB());
}
}
}
// Usage: circle.draw(redColor); // Dispatches only on Circle, checks color manually
// Multiple dispatch example (pseudocode, inspired by MultiJava style)
multimethod draw(Shape s, Color col);
draw(Circle, RedColor) {
renderCircleRed(); // Specific to Circle and RedColor
}
draw(Rectangle, BlueColor) {
renderRectangleBlue(); // Specific to Rectangle and BlueColor
}
draw(Shape, Color) { // Default for other combinations
renderBasic(col.getRGB());
}
// Usage: draw(circle, redColor); // Dispatches on both types automatically
// Multiple dispatch example (pseudocode, inspired by MultiJava style)
multimethod draw(Shape s, Color col);
draw(Circle, RedColor) {
renderCircleRed(); // Specific to Circle and RedColor
}
draw(Rectangle, BlueColor) {
renderRectangleBlue(); // Specific to Rectangle and BlueColor
}
draw(Shape, Color) { // Default for other combinations
renderBasic(col.getRGB());
}
// Usage: draw(circle, redColor); // Dispatches on both types automatically
Role of Argument Types
In multiple dispatch systems, the types of all arguments serve as the primary basis for selecting the appropriate method, extending beyond single-argument dispatch by considering combinations across positions. Type hierarchies, constructed through inheritance and subtyping relations, enable this selection by allowing methods defined for supertypes to apply to subtypes, with the system prioritizing the most specific matching method that is compatible with the actual argument types. For example, in Julia, a method defined forNumber arguments can be overridden by a more specific one for Float64 and Int64, ensuring the tightest fit without implicit conversions.[1] This most-specific matching resolves the dispatch by traversing the type lattice, where subtype relations (denoted by <: in Julia) define the hierarchy, such as Integer <: Real <: Number.[12]
Similarly, the Common Lisp Object System (CLOS) employs class hierarchies for multimethod specialization, ordering applicable methods by their specificity to argument classes and selecting the combination that provides the most precise match. In both cases, the hierarchy allows for polymorphic behavior while maintaining predictability through explicit subtype declarations, preventing unintended method invocations on incompatible types.
Multiple dispatch implementations typically rely on nominal typing, where types are distinguished by their explicit names and declarations rather than their internal structure, as exemplified in Julia's type system. This name-based approach enhances dispatch precision by allowing developers to define and reference types explicitly, avoiding ambiguities that might arise from shape-based matching.[12] In contrast, structural typing, which equates types based on compatible field layouts or behaviors without regard to names, is rarer in multiple dispatch due to its potential to introduce less predictable resolution in multi-argument scenarios; however, it can complement nominal systems in hybrid designs for flexible subtyping.[13] Nominal typing in CLOS further reinforces this by tying method applicability directly to class names in the inheritance graph.
Support for composite types like unions and intersections refines dispatch granularity by enabling methods tailored to specific argument combinations. In Julia, union types such as Union{Int64, Float64} allow a single method signature to handle multiple possibilities for an argument, with dispatch selecting based on the concrete type at runtime while still respecting hierarchy specificity.[14] Intersections, implicitly supported through subtyping constraints, permit even finer control, such as matching types that satisfy multiple hierarchical branches simultaneously. For instance, a method might dispatch on T <: Real where T to target numeric subtypes, combining union-like flexibility with intersection precision for optimized overloads.
In statically typed languages with multiple dispatch, type inference is essential for efficient implementation, as it analyzes code to deduce argument types ahead of time, enabling the preconstruction of dispatch tables that map type tuples directly to methods without runtime overhead. This inference process, often leveraging whole-program analysis, ensures that dispatch remains fast even for complex hierarchies, as seen in extensions to hybrid static-dynamic systems.[15] Although Julia is dynamically typed, its advanced inference engine approximates static benefits by specializing code paths based on inferred types, aiding dispatch table optimization for common argument patterns.
Theoretical Foundations
Multimethods and Generality
A multimethod is a generic function that supports multiple specialized implementations, known as methods, each defined by a signature specifying the types of its arguments; at runtime, the appropriate method is selected based on the actual types of the arguments provided. This mechanism, pioneered in the Common Lisp Object System (CLOS), decouples the function's interface from its implementations, allowing methods to be added independently without modifying the generic function itself. Compared to single-dispatch polymorphism, which relies on the type of a single receiver object, multimethods offer greater generality by considering the types of all arguments, facilitating open extension of behavior without subclassing or altering existing code.[16] This extensibility aligns with aspect-oriented programming, where cross-cutting concerns can be modularized through method additions, and rule-based programming, where dispatch logic can evolve incrementally across modules.[17] For instance, new methods can be defined for unforeseen type combinations, promoting modular and retroactive design in large systems.[18] In CLOS, method combination provides further flexibility by defining how multiple applicable methods interact, using strategies such as before methods for pre-execution setup, after methods for post-execution cleanup, and around methods for enclosing primary methods with additional logic. These combinations enable composed behaviors, such as logging or transaction management, without invasive code changes, enhancing the system's adaptability.[17] Predicate dispatch extends the multimethod model by allowing selection based on arbitrary predicates—such as field values or conditions—beyond mere type signatures, unifying traditional dispatch with pattern matching for more expressive control flow.[19] This generalization preserves multimethod semantics while broadening applicability to domains requiring fine-grained, context-sensitive dispatching.[19]Dispatch Resolution Algorithms
Dispatch resolution in multiple dispatch systems typically employs most-specific matching to select the appropriate method from a set of applicable candidates. This algorithm identifies the method whose type signatures are the narrowest—meaning the most precise or restrictive—among those compatible with the runtime types of the arguments, leveraging subtype relations in the type hierarchy. For instance, if arguments are of typesCar and Bike, both subtypes of Vehicle, the system prefers a method specialized to (Car, Bike) over a more general (Vehicle, Vehicle). This approach ensures that specialized behaviors override generic ones, promoting modularity and extensiveness.[2][1][20]
Exclusion mechanisms handle inapplicable methods by first filtering candidates based on whether all argument types satisfy the method's specializers (type requirements). Methods failing this check are excluded from consideration, reducing the search space. For the remaining applicable methods, ordering relies on linearization of type hierarchies to establish a consistent precedence among classes, particularly in the presence of multiple inheritance. The C3 linearization algorithm, originally developed for the Dylan programming language, achieves this by merging local class precedence orders with those of superclasses in a monotonic manner—preserving the order of direct superclasses and avoiding non-local precedence violations. It constructs a class precedence list (CPL) by iteratively selecting the next class that appears as a head in some parent's linearization without contradicting prior choices, ensuring unambiguous specificity comparisons. This linearization enables efficient sorting of methods from most to least specific during resolution.[2][20][21]
To optimize repeated invocations, many implementations use cache-based techniques such as inline caching or dispatch tables. Inline caching stores the types of arguments from a prior call alongside the selected method at the call site, allowing subsequent calls with matching types to bypass full resolution. For multiple dispatch, caches often key on tuples of argument types, with polymorphic variants handling a small set of common type combinations to limit cache size. Invalidation occurs when type hierarchies change (e.g., new subtypes added), triggering recompilation or cache flushes to maintain correctness. Julia, for example, caches compiled method specializations per type signature, accelerating dispatch while supporting dynamic type extensions.[1][22]
The following pseudocode outlines a basic dispatch resolution process, drawing from standard implementations like those in CLOS and Julia:
function resolve_method(generic_function, args):
arg_types = get_runtime_types(args)
applicable_methods = []
for method in generic_function.methods:
if all arg_types[i] <: method.signature[i] for i in 1 to length(args): // <: denotes subtyping
applicable_methods.append(method)
end
end
if applicable_methods.empty:
error "No applicable method"
end
if length(applicable_methods) == 1:
return applicable_methods[1]
end
// Find the most specific method using component-wise subtyping
most_specific = null
for candidate in applicable_methods:
is_more_specific = true
for other in applicable_methods:
if other != candidate:
// Check if candidate is more specific than other
all_subtype = true
strict_some = false
for i in 1 to length(arg_types):
if not (candidate.signature[i] <: other.signature[i]):
all_subtype = false
break
if candidate.signature[i] < other.signature[i]: // strict subtype
strict_some = true
end
if all_subtype and strict_some:
continue // candidate more specific than this other
else:
is_more_specific = false
break
end
end
if is_more_specific:
if most_specific != null:
error "Ambiguous method match" // multiple most-specific
end
most_specific = candidate
end
if most_specific == null:
error "No unique most-specific method"
end
return most_specific
function resolve_method(generic_function, args):
arg_types = get_runtime_types(args)
applicable_methods = []
for method in generic_function.methods:
if all arg_types[i] <: method.signature[i] for i in 1 to length(args): // <: denotes subtyping
applicable_methods.append(method)
end
end
if applicable_methods.empty:
error "No applicable method"
end
if length(applicable_methods) == 1:
return applicable_methods[1]
end
// Find the most specific method using component-wise subtyping
most_specific = null
for candidate in applicable_methods:
is_more_specific = true
for other in applicable_methods:
if other != candidate:
// Check if candidate is more specific than other
all_subtype = true
strict_some = false
for i in 1 to length(arg_types):
if not (candidate.signature[i] <: other.signature[i]):
all_subtype = false
break
if candidate.signature[i] < other.signature[i]: // strict subtype
strict_some = true
end
if all_subtype and strict_some:
continue // candidate more specific than this other
else:
is_more_specific = false
break
end
end
if is_more_specific:
if most_specific != null:
error "Ambiguous method match" // multiple most-specific
end
most_specific = candidate
end
if most_specific == null:
error "No unique most-specific method"
end
return most_specific
Formal Semantics
Formal semantics for multiple dispatch typically involve mathematical models that define how argument types determine method selection and execution, often through operational or static type systems integrated with type theory. These models ensure type safety and unambiguity in dispatch resolution, generalizing single-dispatch mechanisms to tuples of argument types. Key contributions include frameworks for symmetric dispatch with polymorphism and variance, as well as specificity-based selection in modular settings.[23][24] One approach to modeling multiple dispatch uses a mapping from argument type tuples to method semantics, where applicable methods are ordered by specificity derived from subtype relations forming a partial order. For a set of overloaded methods, the specificity relation holds if applies to every type tuple that does, ensuring a unique most specific method (the meet in the semilattice) is selected to avoid ambiguities. This partial order on declarations supports modular type checking by enforcing rules like the Meet Rule (a common lower bound exists for any pair of applicable declarations) and the No Duplicates Rule (no two declarations are equally specific). Such mappings provide a denotational interpretation where the semantics of a call on types denote the behavior of the most specific applicable method.[24] Integration with type theory often incorporates parametric polymorphism and variance to handle multiple inheritance and generic types in dispatch. In systems supporting symmetric multiple dispatch, types are annotated with variance (covariant +, contravariant -, or invariant =), affecting subtyping and method applicability; for instance, the domain of a generic function is an existential type , and the arrow type is , with subtyping rules preserving dispatch correctness. The dispatch function, such as , selects method as the most specific applicable visible instance based on static types, extended dynamically via run-time ilks (instance kinds) to ensure type soundness. These integrations enable polymorphic dispatch while proving properties like progress and preservation.[23] Seminal research on formal semantics for multiple dispatch in the Common Lisp Object System (CLOS) includes an algebraic specification of method combination by Olthoff and Kempf (1989).[25] The metaobject protocol, as detailed by Kiczales et al. (1991), provides a conceptual model for generic functions and multimethod dispatch, separating dispatch logic from method bodies to support customizable resolution.[26] Later works built on this by providing type-safe extensions, such as modular multimethods with parametric polymorphism, ensuring separate compilation and inheritance compatibility.[24]Advantages and Challenges
Expressiveness and Modularity
Multiple dispatch enhances polymorphism by enabling method selection based on the runtime types of multiple arguments, allowing developers to define behaviors for specific combinations of argument types—such as type intersections—without relying on deep or complex inheritance hierarchies. This approach provides a more flexible alternative to single dispatch, where behavior is determined solely by the receiver's type, as it supports generic functions that adapt to diverse input combinations seamlessly. For instance, in languages like Julia, this facilitates defining operations that naturally handle interactions between unrelated types, promoting code reuse and reducing the need for type-specific overrides scattered across class definitions.[2] Modularity is significantly improved through multiple dispatch, as it allows methods to be added or extended independently by different modules without modifying existing class definitions, thereby reducing coupling between components. This is particularly evident in systems supporting open classes, where new methods can be declared in separate compilation units to augment the interface of predefined types, enabling extensible designs without recompilation or subclass proliferation. In the MultiJava extension to Java, open classes combined with symmetric multiple dispatch exemplify this by permitting clients to add behaviors to library classes modularly, fostering collaborative development while maintaining encapsulation. Such mechanisms align with dynamic languages like Common Lisp, where open classes further decouple method definitions from type declarations, enhancing overall system maintainability.[27][27] Multiple dispatch naturally enables design patterns that involve dispatching on multiple arguments, such as double dispatch scenarios in computational geometry, where operations like intersection or collision detection depend on the types of two shapes. For example, a genericintersect function can define specialized methods for pairs like Circle and Rectangle, avoiding the need for manual type checks or chained virtual calls that complicate single-dispatch implementations. This direct support for multi-argument polymorphism streamlines pattern realization, making code more intuitive and less prone to errors in type-dependent computations.[2]
A key case study illustrating these benefits is the Visitor pattern, where multiple dispatch mitigates the fragility inherent in traditional single-dispatch implementations. In single-dispatch languages like Java, the Visitor pattern requires predefined visit methods in each element class and corresponding visitor subclasses for new operations, leading to tight coupling and maintenance challenges when adding new element types or operations. Multiple dispatch, however, allows defining visit-like methods directly on argument type combinations, such as visit(Element, Visitor), enabling extensions without altering existing classes—thus avoiding the pattern's extensibility limitations and promoting more robust, modular designs. This approach, as demonstrated in practical extensions like MultiJava, transforms the Visitor into a simpler multimethod, reducing boilerplate and enhancing long-term evolvability.[2][27]
Ambiguity and Resolution
In multiple dispatch systems, ambiguity arises when multiple methods have overlapping type signatures that are equally applicable to the given arguments, preventing the selection of a unique most specific method. For instance, consider two methods defined asg(x::Float64, y) and g(x, y::Float64); when called with g(2.0, 3.0), both match equally well since Float64 is a subtype of Any (or Object in other languages), leading to no clear winner in specificity.[1] Similarly, in systems like the Python multipledispatch library, signatures such as (float, object) and (object, float) create ambiguity for inputs like (2.0, 10), as neither is more specific overall.[28]
Resolution strategies vary across implementations but commonly involve user-defined ordering, specialization, or error handling to disambiguate. In the Common Lisp Object System (CLOS), ambiguities are addressed through auxiliary methods like :before and :after, which execute in a specific order around a primary method—most specific :before methods run first, followed by the primary, and then :after methods from least to most specific—allowing developers to layer behavior without altering dispatch core.[29] Default methods provide fallback behavior, while :around methods can wrap others using call-next-method to resolve or modify execution flow. In contrast, Julia reports ambiguities as MethodError at runtime but encourages resolution by defining a more specific method for the conflicting case, such as g(x::Float64, y::Float64), which takes precedence due to its tighter signature.[1] The multipledispatch library issues warnings at definition time and resolves by favoring the first-registered most specific implementation, though unresolved cases may select pseudo-randomly at runtime.[28] Generic function specialization, where developers explicitly add methods for ambiguous combinations, is a common approach across systems to ensure unique matches.[2]
Detection of ambiguities often occurs at compile-time in statically typed languages through analysis of type hierarchies, enabling early warnings before runtime execution. Julia, for example, performs static checks during method definition to flag potential conflicts based on subtype relations, allowing preemptive fixes.[1] Runtime resolution predominates in dynamic languages like CLOS, where dispatch algorithms evaluate argument types on-the-fly using class precedence lists to compute specificity, potentially incurring overhead but providing flexibility.[29]
Best practices to prevent ambiguities emphasize designing orthogonal type hierarchies and explicit annotations. Developers should define more specific methods before general ones to establish clear precedence, use clear type signatures to minimize overlaps, and incorporate exclusion mechanisms—such as conditional dispatch or wrappers—to isolate conflicting cases. In Julia, promoting arguments to common types via functions like promote helps avoid ambiguities in numerical code, while in CLOS, adhering to linear class precedence lists reduces inheritance-related overlaps.[1][28][29]
Performance Implications
Multiple dispatch incurs runtime overhead compared to single dispatch due to the necessity of inspecting types across all arguments to resolve the most specific method, a process that generally scales linearly with the number of arguments, O(n), as each type must be checked during resolution. This overhead arises from dynamic method selection in the presence of multiple candidates, potentially involving table traversals or applicability tests for each call.[30] Optimization techniques address this by precomputing dispatch tables that encode type combinations for rapid lookups, achieving constant-time selection after initialization and reducing space through compression algorithms that eliminate redundant entries. In just-in-time (JIT) compiled systems like Julia, type inference enables method specialization at runtime, compiling dedicated code for concrete argument types and minimizing repeated dispatch costs to near-zero for type-stable invocations.[31] Benchmark studies in dynamic environments reveal notable overhead relative to single dispatch, attributable to the added complexity of multi-argument resolution, though optimizations like specialization in Julia mitigate this, yielding performance within 0.9x to 6x of optimized C code across numerical benchmarks. These costs are further reduced in static or hybrid systems where dispatch can be resolved at compile time.[30][31] This performance trade-off supports greater code expressiveness and modularity, as multiple dispatch allows extensible designs that prioritize long-term maintainability over minimal per-call latency, reconciling dynamism with efficiency through targeted optimizations.[31]Language Support and Implementations
Native Multiple Dispatch
Native multiple dispatch is a built-in feature in several programming languages, where method or function selection occurs dynamically based on the runtime types of all arguments, enabling flexible and expressive polymorphism without relying on single-argument dispatching. This approach contrasts with traditional single dispatch in object-oriented languages and has been integrated into language designs to support advanced generic programming, particularly in domains requiring high expressiveness and performance. Languages with native support include Common Lisp, Julia, Raku, and Groovy, alongside historical examples like Dylan and Fortress. In Common Lisp, the Common Lisp Object System (CLOS) provides comprehensive multimethod support through thedefmethod macro, which defines methods on generic functions using lambda lists that include type specifiers for each argument. For instance, methods can be specialized on classes or types for multiple parameters, allowing dispatch to resolve based on combinations of argument types. CLOS has offered this full multimethod capability since its standardization in the late 1980s as part of the Common Lisp ANSI specification.[32][33]
Julia integrates multiple dispatch as a foundational element, dispatching functions based on the types of all arguments to select the most specific method. Methods are defined with type signatures for parameters, and tools like @code_warntype assist in analyzing dispatch and type inference for optimization. Developed for high-performance numerical and scientific computing, Julia has supported this feature since its initial release in 2012. By 2025, Julia's multiple dispatch has solidified its role as a leading language in scientific computing, powering applications in high-performance computing (HPC) environments and data analysis workflows.[1][34]
Raku (formerly Perl 6) employs multi sub declarations with type constraints in signatures to enable multiple dispatch, where the language selects the most applicable candidate based on argument types and counts at runtime. This design emphasizes expressiveness for multi-paradigm programming, supporting procedural, object-oriented, and functional styles. Raku introduced this native support upon its first stable release in 2015.[35]
Groovy supports dynamic multiple dispatch, also known as multimethods, through method overloading where the runtime selects the implementation based on argument types, integrated with its JVM-based execution model. Developers can define multiple methods with the same name but differing parameter types. This feature has been part of Groovy since its initial release in 2003.[36][37]
Historically, the Dylan language featured multiple dispatch as a core capability, with methods defined on generic functions that consider all argument types for selection, influencing later designs in the 1990s. Similarly, Fortress, a high-performance language for scientific computing developed by Sun Microsystems starting in 2001, incorporated multiple dynamic dispatch for overloaded functions and methods, though the project was discontinued around 2012. These earlier implementations highlight the evolution of native multiple dispatch toward modern applications like those in Julia. Cecil, a prototype-based object-oriented language developed in the 1990s, natively supported symmetric multimethods, allowing dispatch based on the runtime types of all arguments and influencing subsequent language designs.[38][39][2]
Library-Based Extensions
Library-based extensions provide mechanisms to retrofit multiple dispatch capabilities into programming languages that do not natively support it, typically through high-level abstractions like decorators, wrappers, or type-constrained method definitions. These libraries leverage the host language's dynamic features or type systems to enable type-based dispatching on multiple arguments, often with minimal syntax overhead. Such approaches contrast with native implementations by requiring explicit library integration but offer flexibility for retrofitting existing codebases. In Python, the multipledispatch library implements multiple dispatch using a decorator-based syntax, allowing functions to be overloaded based on argument types. Developers annotate dispatch variants with the @dispatch decorator followed by type signatures, such as @dispatch(int, str), enabling efficient runtime selection via static analysis and caching. The library remains active as of 2025, with its latest release on June 27, 2023 (version 1.0.0) supporting Python 3.x versions, and it integrates seamlessly with Python's type hints from the typing module for better static checking and IDE support.[40][41] For JavaScript, libraries like multimethod.js introduce Clojure-inspired multimethods that support dynamic dispatch on prototype-based types. This library defines multimethods via a defmulti function that specifies a dispatch key (e.g., based on object properties or types), with defmethod adding implementations for specific cases, handling JavaScript's loose typing through functional composition. It remains available and usable in modern Node.js and browser environments as of 2025, though its core development has been stable since its initial release, emphasizing lightweight integration without altering the language's prototype chain.[42] In C#, multiple dispatch lacks robust native library support but can be extended using delegates combined with runtime type checks or the dynamic keyword introduced in .NET 4.0 and optimized in .NET 5 (released 2020), which enables runtime method resolution across multiple arguments via reflection or expression trees. Custom libraries or patterns, such as those emulating multimethods through dictionary-based dispatchers, provide further enhancements, though adoption is limited compared to pattern-based solutions like the visitor pattern for double dispatch. These extensions integrate with C#'s strong typing but incur performance overhead due to dynamic invocation.[43] In Racket, multiple dispatch can be implemented using libraries such as the "multimethods" package, which provides a simple and safe system for defining multimethods with dispatch based on argument types, extending Racket's struct and generic capabilities without native language support.[44] Other notable extensions include MooseX::MultiMethods for Perl, which builds on the Moose object system to provide multimethod dispatch using type constraints. It extends Moose's method keyword with a multi variant, allowing definitions like multi method foo(Int y), where dispatch occurs based on runtime argument types matching Moose types. This library, part of the broader Moose ecosystem, facilitates integration in object-oriented Perl code without requiring language modifications.[45] Comparisons across these libraries highlight varying maturity and integration ease: multipledispatch in Python offers the most seamless experience due to its decorator simplicity and type hint compatibility, making it suitable for data science and scientific computing workflows; multimethod.js in JavaScript provides functional elegance but requires familiarity with dispatch keys for prototype handling, with easier integration in functional paradigms; C# extensions via dynamic or delegates are more verbose and performance-sensitive, best for scenarios needing compile-time safety; and MooseX::MultiMethods excels in Perl's Moose-heavy projects for its constraint-based dispatch but assumes prior Moose adoption. Overall, Python's offering demonstrates the highest maturity in terms of documentation, community usage, and cross-version stability as of 2025.[40][42][45]Emulation in Other Languages
In languages without built-in support for multiple dispatch, programmers can emulate the behavior using language-specific idioms that leverage type systems, pointers, or design patterns to approximate runtime or compile-time selection based on multiple argument types. These approaches often involve manual resolution mechanisms, which provide flexibility but introduce complexities in implementation and evolution.[46] In C++, static emulation can be achieved through template metaprogramming combined with Substitution Failure Is Not an Error (SFINAE), allowing compile-time dispatch on multiple argument types by enabling or disabling template overloads based on type traits. For instance, SFINAE detects type compatibility to select appropriate function templates without runtime overhead, as detailed in standard C++ references. Runtime emulation typically employs the visitor pattern, where a base class defines a virtualaccept method that calls a corresponding visit method on a visitor object, enabling double dispatch on the types of both the visitor and the visited object; this can be enhanced with std::variant and std::visit (introduced in C++17) to handle heterogeneous types in a type-safe manner, dispatching to overloads based on the variant's held type. The visitor approach resolves the expression problem partially by separating operations from data structures but requires modifications to existing hierarchies when adding new types.[47][46]
Java, being strictly single-dispatch, emulates multiple dispatch primarily through the double dispatch mechanism embedded in the visitor pattern, where an element interface declares an accept(Visitor v) method that invokes v.visit(this), allowing the visitor's type to influence the dispatched method alongside the element's type. Interfaces and abstract factories can extend this by defining polymorphic hierarchies for arguments, such as creating factory methods that return type-specific visitors, though this remains limited to two-argument cases without reflection, which incurs performance costs and type unsafety. These techniques enable operations like tree traversals but cannot fully generalize to arbitrary arity without excessive subclassing.[48][49]
In C, lacking classes or templates, emulation relies on manual type tagging within structs—such as embedding an enum or integer tag to indicate type—and function pointers stored in struct arrays to simulate virtual tables (vtables) for dispatch. Resolution occurs via explicit switches on tags followed by indirect calls through the appropriate function pointer table, effectively creating a multi-level dispatch table for argument combinations; this approach is common in low-level libraries for polymorphic behavior, like event systems. For example, a base struct might contain a type_tag and a vtable array of function pointers tailored to expected argument types.[50][51]
The D programming language approximates multiple dispatch using mixins for code generation and the alias this declaration to enable implicit type conversions and overload resolution across multiple types, particularly effective for static cases where compile-time introspection selects implementations. Mixins allow injecting type-specific functions into a class, while alias this treats one type as another for operator and method lookup, facilitating dispatch-like behavior without full runtime polymorphism; this is superior to C++ templates for readability in static scenarios but falls short for dynamic hierarchies.
Common pitfalls in these emulations include significant boilerplate code for defining dispatch tables, visitors, or mixins, leading to maintenance overhead as new types require updates across multiple sites, potentially violating the open-closed principle and exacerbating the expression problem. Performance trade-offs arise from indirect calls and type checks, though static variants minimize runtime costs compared to native implementations.[46]
Practical Usage
Applications in Paradigms
In object-oriented programming, multiple dispatch enhances traditional inheritance hierarchies by enabling symmetric polymorphism, where method selection depends on the runtime types of all arguments rather than solely the receiver object. This approach allows for more flexible and modular designs, as interactions between objects of different classes can be defined without tightly coupling them through single-dispatch mechanisms or visitor patterns.[2] Furthermore, it reduces reliance on mixins, which often complicate inheritance by requiring manual composition of behaviors across unrelated classes; instead, multiple dispatch directly specializes methods on argument type combinations, avoiding such workarounds and promoting cleaner code organization.[2] In functional programming, multiple dispatch supports the creation of generic functions that behave like higher-order functions, dispatching based on argument types to handle diverse inputs uniformly. For instance, in Julia, this enables variants of operations like map-reduce by defining methods that specialize on container types and element types, facilitating composable and type-safe abstractions without explicit type checks.[1] Such integration aligns with functional principles of immutability and pure functions, as dispatch ensures that transformations (e.g., element-wise addition across arrays) are optimized and extensible across type domains.[1] Within scientific computing, multiple dispatch proves particularly valuable for developing optimized numerical kernels that operate on mixed-type arrays, where computations must adapt to varying data representations such as scalars, vectors, or matrices with units or uncertainties. In Julia, this mechanism allows methods to be selected based on array element types, enabling efficient implementations for domain-specific operations like geometric calculations or simulations without sacrificing performance through generic fallbacks.[52] By dispatching on multiple arguments, including array structures and numerical precisions, it supports high-performance computing workflows that integrate heterogeneous data seamlessly.[52] In other paradigms, multiple dispatch plays a key role in rule-based systems by facilitating pattern matching on multiple argument types to select applicable production rules, akin to dispatching in expert systems where conditions trigger specific actions.[53] Similarly, in aspect-oriented programming, extensions like predicate dispatch generalize multiple dispatch to weave aspects based on combined argument predicates, enhancing modularity for cross-cutting concerns such as logging or security without invasive modifications to core logic.[54]Real-World Examples
In geometry libraries, multiple dispatch facilitates efficient computation of intersections between diverse shape types by selecting the appropriate algorithm based on the runtime types of both operands, avoiding the need for explicit type checks or visitor patterns. For instance, intersecting a circle with a line requires a different mathematical approach than intersecting a polygon with a rectangle, and multiple dispatch resolves this by dispatching to specialized methods for each pair, such asintersect(Circle, Line) or intersect([Polygon](/page/Polygon), [Rectangle](/page/Rectangle)). This design promotes extensibility, allowing new shapes like triangles to be added without modifying existing code, as demonstrated in use cases where symmetric handling of arguments (e.g., intersect([Square](/page/Circle_Square), [Circle](/page/Circle)) and intersect(Circle, [Square](/page/Circle_Square))) ensures comprehensive coverage of interactions.[55]
In game engines, multiple dispatch supports handling entity interactions, such as collisions, by dispatching based on the types of involved entities rather than relying on deep inheritance hierarchies. A classic example is vehicle collision detection, where collide(Vehicle, Vehicle) can specialize to behaviors like collide(Player, [Enemy](/page/Enemy)) for combat logic or collide(Player, Item) for pickup mechanics, enabling polymorphic responses without subclass proliferation. This approach simplifies entity systems by decoupling behavior from class structure, allowing flexible interactions in dynamic environments like player-enemy engagements or item collections.
For data processing in extract-transform-load (ETL) pipelines, multiple dispatch enables generic serialization of mixed-type inputs by selecting format-specific handlers based on the types of data elements being processed. In sensor data frameworks, for example, location stack systems[2] use multiple dispatch to transform and serialize heterogeneous inputs like geographical coordinates and timestamps into unified streams, handling combinations such as numeric-location or string-coordinate pairs without manual type branching. This supports scalable processing in pipelines where inputs vary across sources, ensuring type-safe and efficient data flow.
Empirical evidence from an analysis of nine programs across six languages supporting multiple dispatch—CLOS, Dylan, Cecil, Diesel, Nice, and MultiJava—reveals that multimethods account for approximately 3% of generic function usage, with single-dispatch methods comprising about 30% and the majority (65-93%) being monomorphic.[3] This study, examining real-world applications including data processing frameworks, indicates that while multiple dispatch is not ubiquitous, it provides targeted benefits in scenarios requiring type-dependent behavior, such as the entity interactions and serializations noted above.
