Multiple dispatch

Last updated

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.

Contents

Understanding dispatch

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.

The Common Lisp Object System (CLOS) is an early and well-known example of multiple dispatch. Another notable example of the use of multiple dispatch is the Julia programming language.

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

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

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

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

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

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

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):

LanguageAverage # methods (DR)Choice ratio (CR)Degree of specialization (DoS)
Cecil [2] 2.3363.301.06
Common Lisp (CMU) [2] 2.036.341.17
Common Lisp (McCLIM) [2] 2.3215.431.17
Common Lisp (Steel Bank) [2] 2.3726.571.11
Diesel [2] 2.0731.650.71
Dylan (Gwydion) [2] 1.7418.272.14
Dylan (OpenDylan) [2] 2.5143.841.23
Julia [3] 5.8651.441.54
Julia (operators only) [3] 28.1378.062.01
MultiJava [2] 1.508.921.02
Nice [2] 1.363.460.33

Theory

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

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

C#

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.

Csharp ColliderLibrary.svg

The example below uses features introduced in C# 9 and C# 10.

usingstaticColliderLibrary;Console.WriteLine(Collide(newAsteroid(101),newSpaceship(300)));Console.WriteLine(Collide(newAsteroid(10),newSpaceship(10)));Console.WriteLine(Collide(newSpaceship(101),newSpaceship(10)));stringCollide(SpaceObjectx,SpaceObjecty)=>x.Size>100&&y.Size>100?"Big boom!":CollideWith(xasdynamic,yasdynamic);// Dynamic dispatch to CollideWith methodclassColliderLibrary{publicstaticstringCollideWith(Asteroidx,Asteroidy)=>"a/a";publicstaticstringCollideWith(Asteroidx,Spaceshipy)=>"a/s";publicstaticstringCollideWith(Spaceshipx,Asteroidy)=>"s/a";publicstaticstringCollideWith(Spaceshipx,Spaceshipy)=>"s/s";}abstractrecordSpaceObject(intSize);recordAsteroid(intSize):SpaceObject(Size);recordSpaceship(intSize):SpaceObject(Size);

Output:

Big boom!a/ss/s

Groovy

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)*/classProgram{staticvoidmain(String[]args){printlnCollider.collide(newAsteroid(101),newSpaceship(300))printlnCollider.collide(newAsteroid(10),newSpaceship(10))printlnCollider.collide(newSpaceship(101),newSpaceship(10))}}classCollider{staticStringcollide(SpaceObjectx,SpaceObjecty){(x.size>100&&y.size>100)?"big-boom":collideWith(x,y)// Dynamic dispatch to collideWith method}privatestaticStringcollideWith(Asteroidx,Asteroidy){"a/a"}privatestaticStringcollideWith(Asteroidx,Spaceshipy){"a/s"}privatestaticStringcollideWith(Spaceshipx,Asteroidy){"s/a"}privatestaticStringcollideWith(Spaceshipx,Spaceshipy){"s/s"}}classSpaceObject{intsizeSpaceObject(intsize){this.size=size}}@InheritConstructorsclassAsteroidextendsSpaceObject{}@InheritConstructorsclassSpaceshipextendsSpaceObject{}

Common Lisp

In a language with multiple dispatch, such as Common Lisp, it might look more like this (Common Lisp example shown):

(defmethodcollide-with((xasteroid)(yasteroid));; deal with asteroid hitting asteroid)(defmethodcollide-with((xasteroid)(yspaceship));; deal with asteroid hitting spaceship)(defmethodcollide-with((xspaceship)(yasteroid));; deal with spaceship hitting asteroid)(defmethodcollide-with((xspaceship)(yspaceship));; deal with spaceship hitting spaceship)

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

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:

abstracttypeSpaceObjectendstructAsteroid<:SpaceObjectsize::IntendstructSpaceship<:SpaceObjectsize::Intendcollide_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

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.

subsetMassofRealwhere0 ^..^ Inf;  roleStellar-Object {     hasMass$.massisrequired;     methodname () returnsStr {...}; } classAsteroiddoesStellar-Object {     methodname () { 'an asteroid' } } classSpaceshipdoesStellar-Object {     hasStr$.name = 'some unnamed spaceship'; } myStr@destroyed = < obliterateddestroyedmangled >; myStr@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.multisubinfix:« <=> » ( Stellar-Object:D$a, Stellar-Object:D$b ) { $a.mass <=> $b.mass } multisubinfix:« < » ( Stellar-Object:D$a, Stellar-Object:D$b ) { $a.mass < $b.mass } multisubinfix:« > » ( Stellar-Object:D$a, Stellar-Object:D$b ) { $a.mass > $b.mass } multisubinfix:« == » ( 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.protosubcollide ( 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.multisubcollide ( $a, $bwhere$a < $b ) {     say"$a.name() was @destroyed.pick() by $b.name()"; } multisubcollide ( $a, $bwhere$a > $b ) {     # redispatch to the previous candidate with the arguments swappedsamewith$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. )multisubcollide ( $a, $b ) {     # randomize the ordermy ($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.multisubcollide ( Spaceship$a, Spaceship$bwhere$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)`.multisubcollide ( Asteroid $ (:mass($a)), Asteroid $ (:mass($b)) ){     say"two asteroids collided and combined into one larger asteroid of mass { $a + $b }"; }  mySpaceship$Enterprise .= new(:mass(1),:name('The Enterprise')); collideAsteroid.new(:mass(.1)), $Enterprise; collide$Enterprise, Spaceship.new(:mass(.1)); collide$Enterprise, Asteroid.new(:mass(1)); collide$Enterprise, Spaceship.new(:mass(1)); collideAsteroid.new(:mass(10)), Asteroid.new(:mass(5)); 

Extending languages with multiple-dispatch libraries

JavaScript

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'classAsteroid{}classSpaceship{}constcollideWith=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'classAsteroid{}classSpaceship{}typeCollideWith=Multi&{(x:Asteroid,y:Asteroid):void(x:Asteroid,y:Spaceship):void(x:Spaceship,y:Asteroid):void(x:Spaceship,y:Spaceship):void}constcollideWith: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

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.

frommultimethodsimportDispatchfromgame_objectsimportAsteroid,Spaceshipfromgame_behaviorsimportas_func,ss_func,sa_funccollide=Dispatch()collide.add_rule((Asteroid,Spaceship),as_func)collide.add_rule((Spaceship,Spaceship),ss_func)collide.add_rule((Spaceship,Asteroid),sa_func)defaa_func(a,b):"""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 Python 2.4 decorators, Guido van Rossum produced a sample implementation of multimethods [15] with a simplified syntax:

@multimethod(Asteroid,Asteroid)defcollide(a,b):"""Behavior when asteroid hits a asteroid."""# ...define new behavior...@multimethod(Asteroid,Spaceship)defcollide(a,b):"""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,

fromplumimportdispatch@dispatchdefcollide(a:Asteroid,b:Asteroid):"""Behavior when asteroid hits a asteroid."""# ...define new behavior...@dispatchdefcollide(a:Asteroid,b:Spaceship):"""Behavior when asteroid hits a spaceship."""# ...define new behavior...# ...define further rules...

Emulating multiple dispatch

C

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:

typedefvoid(*CollisionCase)(void);voidcollision_AA(void){/* handle Asteroid-Asteroid collision  */};voidcollision_AS(void){/* handle Asteroid-Spaceship collision */};voidcollision_SA(void){/* handle Spaceship-Asteroid collision */};voidcollision_SS(void){/* handle Spaceship-Spaceship collision*/};typedefenum{THING_ASTEROID=0,THING_SPACESHIP,THING_COUNT/* not a type of thing itself, instead used to find number of things */}Thing;CollisionCasecollisionCases[THING_COUNT][THING_COUNT]={{&collision_AA,&collision_AS},{&collision_SA,&collision_SS}};voidcollide(Thinga,Thingb){(*collisionCases[a][b])();}intmain(void){collide(THING_SPACESHIP,THING_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>// classesdefclass(Asteroid)// data membersendclassdefclass(Spaceship)// data membersendclass// genericsdefgeneric(_Bool,collide_with,_1,_2);// multimethodsdefmethod(_Bool,collide_with,Asteroid,Asteroid)// deal with asteroid hitting asteroidendmethoddefmethod(_Bool,collide_with,Asteroid,Spaceship)// deal with asteroid hitting spaceshipendmethoddefmethod(_Bool,collide_with,Spaceship,Asteroid)// deal with spaceship hitting asteroidendmethoddefmethod(_Bool,collide_with,Spaceship,Spaceship)// deal with spaceship hitting spaceshipendmethod// example of useintmain(void){OBJa=gnew(Asteroid);OBJs=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++

As of 2021, 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_caststructThing{virtualvoidcollideWith(Thing&other)=0;};structAsteroid:Thing{voidcollideWith(Thing&other){// dynamic_cast to a pointer type returns NULL if the cast fails// (dynamic_cast to a reference type would throw an exception on failure)if(autoasteroid=dynamic_cast<Asteroid*>(&other)){// handle Asteroid-Asteroid collision}elseif(autospaceship=dynamic_cast<Spaceship*>(&other)){// handle Asteroid-Spaceship collision}else{// default collision handling here}}};structSpaceship:Thing{voidcollideWith(Thing&other){if(autoasteroid=dynamic_cast<Asteroid*>(&other)){// handle Spaceship-Asteroid collision}elseif(autospaceship=dynamic_cast<Spaceship*>(&other)){// handle Spaceship-Spaceship collision}else{// default collision handling here}}};

or pointer-to-method lookup table:

#include<cstdint>#include<typeinfo>#include<unordered_map>classThing{protected:Thing(std::uint32_tcid):tid(cid){}conststd::uint32_ttid;// type idtypedefvoid(Thing::*CollisionHandler)(Thing&other);typedefstd::unordered_map<std::uint64_t,CollisionHandler>CollisionHandlerMap;staticvoidaddHandler(std::uint32_tid1,std::uint32_tid2,CollisionHandlerhandler){collisionCases.insert(CollisionHandlerMap::value_type(key(id1,id2),handler));}staticstd::uint64_tkey(std::uint32_tid1,std::uint32_tid2){returnstd::uint64_t(id1)<<32|id2;}staticCollisionHandlerMapcollisionCases;public:voidcollideWith(Thing&other){autohandler=collisionCases.find(key(tid,other.tid));if(handler!=collisionCases.end()){(this->*handler->second)(other);// pointer-to-method call}else{// default collision handling}}};classAsteroid:publicThing{voidasteroid_collision(Thing&other){/*handle Asteroid-Asteroid collision*/}voidspaceship_collision(Thing&other){/*handle Asteroid-Spaceship collision*/}public:Asteroid():Thing(cid){}staticvoidinitCases();staticconststd::uint32_tcid;};classSpaceship:publicThing{voidasteroid_collision(Thing&other){/*handle Spaceship-Asteroid collision*/}voidspaceship_collision(Thing&other){/*handle Spaceship-Spaceship collision*/}public:Spaceship():Thing(cid){}staticvoidinitCases();staticconststd::uint32_tcid;// class id};Thing::CollisionHandlerMapThing::collisionCases;conststd::uint32_tAsteroid::cid=typeid(Asteroid).hash_code();conststd::uint32_tSpaceship::cid=typeid(Spaceship).hash_code();voidAsteroid::initCases(){addHandler(cid,cid,CollisionHandler(&Asteroid::asteroid_collision));addHandler(cid,Spaceship::cid,CollisionHandler(&Asteroid::spaceship_collision));}voidSpaceship::initCases(){addHandler(cid,Asteroid::cid,CollisionHandler(&Spaceship::asteroid_collision));addHandler(cid,cid,CollisionHandler(&Spaceship::spaceship_collision));}intmain(){Asteroid::initCases();Spaceship::initCases();Asteroida1,a2;Spaceships1,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>#include<memory>classThing{public:virtual~Thing(){}};classAsteroid:publicThing{};classSpaceship:publicThing{};register_classes(Thing,Spaceship,Asteroid);declare_method(void,collideWith,(virtual_<Thing&>,virtual_<Thing&>));define_method(void,collideWith,(Thing&left,Thing&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}intmain(){yorel::yomm2::update_methods();std::unique_ptr<Thing>a1(std::make_unique<Asteroid>()),a2(std::make_unique<Asteroid>());std::unique_ptr<Thing>s1(std::make_unique<Spaceship>()),s2(std::make_unique<Spaceship>());// note: types partially erasedcollideWith(*a1,*a2);// Asteroid-Asteroid collisioncollideWith(*a1,*s1);// Asteroid-Spaceship collisioncollideWith(*s1,*a1);// Spaceship-Asteroid collisioncollideWith(*s1,*s2);// Spaceship-Spaceship collisionreturn0;}

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

As of 2021, 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.

// DeclarationMatrixplus(virtual!Matrix,virtual!Matrix);// The override for two DenseMatrix objects@methodMatrix_plus(DenseMatrixa,DenseMatrixb){constintnr=a.rows;constintnc=a.cols;assert(a.nr==b.nr);assert(a.nc==b.nc);autoresult=newDenseMatrix;result.nr=nr;result.nc=nc;result.elems.length=a.elems.length;result.elems[]=a.elems[]+b.elems[];returnresult;}// The override for two DiagonalMatrix objects@methodMatrix_plus(DiagonalMatrixa,DiagonalMatrixb){assert(a.rows==b.rows);double[]sum;sum.length=a.elems.length;sum[]=a.elems[]+b.elems[];returnnewDiagonalMatrix(sum);}

Java

In a language with only single dispatch, such as Java, multiple dispatch can be emulated with multiple levels of single dispatch:

UML class Java single dispatch.svg

interfaceCollideable{voidcollideWith(finalCollideableother);/* These methods would need different names in a language without method overloading. */voidcollideWith(finalAsteroidasteroid);voidcollideWith(finalSpaceshipspaceship);}classAsteroidimplementsCollideable{publicvoidcollideWith(finalCollideableother){// Call collideWith on the other object.other.collideWith(this);}publicvoidcollideWith(finalAsteroidasteroid){// Handle Asteroid-Asteroid collision.}publicvoidcollideWith(finalSpaceshipspaceship){// Handle Asteroid-Spaceship collision.}}classSpaceshipimplementsCollideable{publicvoidcollideWith(finalCollideableother){// Call collideWith on the other object.other.collideWith(this);}publicvoidcollideWith(finalAsteroidasteroid){// Handle Spaceship-Asteroid collision.}publicvoidcollideWith(finalSpaceshipspaceship){// Handle Spaceship-Spaceship collision.}}

Run time instanceof checks at one or both levels can also be used.

Support in programming languages

Primary paradigm

Supporting general multimethods

Via extensions

See also

Related Research Articles

<span class="mw-page-title-main">Dylan (programming language)</span> Multi-paradigm programming language

Dylan is a multi-paradigm programming language that includes support for functional and object-oriented programming (OOP), and is dynamic and reflective while providing a programming model designed to support generating efficient machine code, including fine-grained control over dynamic and static behaviors. It was created in the early 1990s by a group led by Apple Computer.

A visitor pattern is a software design pattern that separates the algorithm from the object structure. Because of this separation new operations can be added to existing object structures without modifying the structures. It is one way to follow the open/closed principle in object-oriented programming and software engineering.

In object-oriented programming, the decorator pattern is a design pattern that allows behavior to be added to an individual object, dynamically, without affecting the behavior of other instances of the same class. The decorator pattern is often useful for adhering to the Single Responsibility Principle, as it allows functionality to be divided between classes with unique areas of concern as well as to the Open-Closed Principle, by allowing the functionality of a class to be extended without being modified. Decorator use can be more efficient than subclassing, because an object's behavior can be augmented without defining an entirely new object.

<span class="mw-page-title-main">Common Lisp Object System</span>

The Common Lisp Object System (CLOS) is the facility for object-oriented programming in ANSI Common Lisp. CLOS is a powerful dynamic object system which differs radically from the OOP facilities found in more static languages such as C++ or Java. CLOS was inspired by earlier Lisp object systems such as MIT Flavors and CommonLoops, although it is more general than either. Originally proposed as an add-on, CLOS was adopted as part of the ANSI standard for Common Lisp and has been adapted into other Lisp dialects such as EuLisp or Emacs Lisp.

In computer programming, a generic function is a function defined for polymorphism.

A method in object-oriented programming (OOP) is a procedure associated with an object, and generally also a message. An object consists of state data and behavior; these compose an interface, which specifies how the object may be used. A method is a behavior of an object parametrized by a user.

In mathematics and computer science, a higher-order function (HOF) is a function that does at least one of the following:

In programming language theory and type theory, polymorphism is the use of a single symbol to represent multiple different types.

In computer science, a type signature or type annotation defines the inputs and outputs of a function, subroutine or method. A type signature includes the number, types, and order of the function's arguments. One important use of a type signature is for function overload resolution, where one particular definition of a function to be called is selected among many overloaded forms.

In computer programming, a default argument is an argument to a function that a programmer is not required to specify. In most programming languages, functions may take one or more arguments. Usually, each argument must be specified in full. Later languages allow the programmer to specify default arguments that always have a value, even if one is not specified when calling the function.

In computer programming, a function object is a construct allowing an object to be invoked or called as if it were an ordinary function, usually with the same syntax. In some languages, particularly C++, function objects are often called functors.

In computer programming, run-time type information or run-time type identification (RTTI) is a feature of some programming languages that exposes information about an object's data type at runtime. Run-time type information may be available for all types or only to types that explicitly have it. Run-time type information is a specialization of a more general concept called type introspection.

In computer science, dynamic dispatch is the process of selecting which implementation of a polymorphic operation to call at run time. It is commonly employed in, and considered a prime characteristic of, object-oriented programming (OOP) languages and systems.

In software engineering, double dispatch is a special form of multiple dispatch, and a mechanism that dispatches a function call to different concrete functions depending on the runtime types of two objects involved in the call. In most object-oriented systems, the concrete function that is called from a function call in the code depends on the dynamic type of a single object and therefore they are known as single dispatch calls, or simply virtual function calls.

In the C++ programming language, new and delete are a pair of language constructs that perform dynamic memory allocation, object construction and object destruction.

The curiously recurring template pattern (CRTP) is an idiom, originally in C++, in which a class X derives from a class template instantiation using X itself as a template argument. More generally it is known as F-bound polymorphism, and it is a form of F-bounded quantification.

Haxe is a high-level cross-platform programming language and compiler that can produce applications and source code for many different computing platforms from one code-base. It is free and open-source software, released under the MIT License. The compiler, written in OCaml, is released under the GNU General Public License (GPL) version 2.

C++11 is a version of the ISO/IEC 14882 standard for the C++ programming language. C++11 replaced the prior version of the C++ standard, called C++03, and was later replaced by C++14. The name follows the tradition of naming language versions by the publication year of the specification, though it was formerly named C++0x because it was expected to be published before 2010.

In computer programming, an anonymous function is a function definition that is not bound to an identifier. Anonymous functions are often arguments being passed to higher-order functions or used for constructing the result of a higher-order function that needs to return a function. If the function is only used once, or a limited number of times, an anonymous function may be syntactically lighter than using a named function. Anonymous functions are ubiquitous in functional programming languages and other languages with first-class functions, where they fulfil the same role for the function type as literals do for other data types.

This article describes the syntax of the C# programming language. The features described are compatible with .NET Framework and Mono.

References

  1. 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.
  2. 1 2 3 4 5 6 7 8 9 10 11 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.
  3. 1 2 3 4 5 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.
  4. 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 .
  5. Castagna, Giuseppe (1996). Object-Oriented Programming: A Unified Foundation. Progress in Theoretical Computer Science. Birkhäuser. p. 384. ISBN   978-0-8176-3905-1.
  6. 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.
  7. 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.
  8. "Using type dynamic (C# Programming Guide)" . Retrieved 2020-05-14.
  9. "Basic concepts" . Retrieved 2020-05-14.
  10. "Dynamic .NET - Understanding the Dynamic Keyword in C# 4" . Retrieved 2020-05-14.
  11. Groovy - Multi-methods
  12. @arrows/multimethod Multiple dispatch in JavaScript/TypeScript with configurable dispatch resolution by Maciej Cąderek.
  13. Coady, Aric, multimethod: Multiple argument dispatching. , retrieved 2021-01-28
  14. multimethods.py Archived 2005-03-09 at the Wayback Machine , Multiple dispatch in Python with configurable dispatch resolution by David Mertz, et al.
  15. "Five-minute Multimethods in Python".
  16. "PEAK-Rules 0.5a1.dev". Python Package Index. Retrieved 21 March 2014.
  17. "PyProtocols". Python Enterprise Application Kit. Retrieved 26 April 2019.
  18. "Reg". Read the docs. Retrieved 26 April 2019.
  19. "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.
  20. "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.
  21. yomm2, Fast, Orthogonal Open Multi-Methods for C++ by Jean-Louis Leroy.
  22. 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.
  23. openmethods, Open Multi-Methods for D by Jean-Louis Leroy.
  24. "Methods". The Julia Manual. Julialang. Archived from the original on 17 July 2016. Retrieved 11 May 2014.
  25. "Multimethods in C# 4.0 With 'Dynamic'" . Retrieved 2009-08-20.
  26. "Cecil Language" . Retrieved 2008-04-13.
  27. "Multimethods in Clojure" . Retrieved 2008-09-04.
  28. Steele, Guy L. (1990). "28". Common LISP: The Language. Bedford, MA, U.S.A: Digital Press. ISBN   978-1-55558-041-4.
  29. "Background and Goals" . Retrieved 2008-04-13.
  30. "The Fortress Language Specification, Version 1.0" (PDF). Archived from the original (PDF) on 2013-01-20. Retrieved 2010-04-23.
  31. "Multimethods in Groovy" . Retrieved 2008-04-13.
  32. "Methods – LassoGuide 9.2" . Retrieved 2014-11-11.
  33. "Visitor Pattern Versus Multimethods" . Retrieved 2008-04-13.
  34. "Nim Manual: Multi-methods" . Retrieved 2022-05-03.
  35. "Perl 6 FAQ" . Retrieved 2008-04-13.
  36. "How S4 Methods Work" (PDF). Retrieved 2008-04-13.
  37. "Multiple Dispatch in Seed7" . Retrieved 2011-04-23.
  38. "TADS 3 System Manual" . Retrieved 2012-03-19.
  39. "VB.Net Multiple Dispatch" . Retrieved 2020-03-31.
  40. "New Features in C#4.0 and VB.Net 10.0". 4 November 2010. Retrieved 2020-03-31.
  41. "Notes for Programming Language Experts" . Retrieved 2016-08-21.
  42. "Multiple dispatch".