You get a bonus - 1 coin for daily activity. Now you have 1 coin

Multimethods, or Multiple Dispatch

Lecture



A multimethod, or multiple dispatch, is a mechanism in programming languages that allows one of several functions to be selected depending on the dynamic types or values of the arguments (for example, method overloading in some programming languages). It is an extension of single dispatch (virtual functions), where the method to call is chosen dynamically based on the actual type of the object on which that method was invoked. Multiple dispatch generalizes dynamic dispatch to cases involving two or more objects.

Multimethods are supported explicitly by “Common Lisp’s Object System” (CLOS).

Dispatch Basics

Program developers typically group source code into named blocks called calls, procedures, subroutines, functions, or methods. A function's code is executed by calling it, which consists of executing the fragment of code denoted by its name. Control is temporarily transferred to the called function; when that function's execution finishes, control is usually transferred back to the instruction following the function call.

Function names are usually chosen to describe their purpose. Sometimes several functions need to be given the same name — typically because they perform conceptually similar tasks but operate on different types of input data. In such cases, the function name alone at the call site is not enough to determine which block of code is being called. In addition to the name, the number and type of the arguments of the function being called are then also used to select a specific implementation.

In more traditional object-oriented programming languages with single dispatch, when a method is called (sending a message in Smalltalk, calling a member function in C++), one of its arguments is treated specially and is used to determine which of the (potentially many) methods with that name should be invoked. In many languages this special argument is marked syntactically; for example, in a number of programming languages the special argument is placed before the dot in the method call:

special.method (other, arguments, here) 

so that lion.sound() would give a roar, and sparrow.sound() would give a chirp.

By contrast, in languages with multiple dispatch, the method chosen — is simply the method whose arguments match the number and type of the arguments in the function call. There is no special argument that “owns” the function or method referenced by a particular call.

The Common Lisp Object System (CLOS) is one of the first and best-known implementations of multiple dispatch.

Data Types

When working with languages in which data types are distinguished at compile time, the choice among the available function variants can be made at compile time. Creating such alternative function variants for selection at compile time is usually called function overloading.

In programming languages that determine data types at run time (late binding), the choice among function variants must be made at run time, based on the dynamically determined types of the function's arguments. Functions whose alternative implementations are selected this way are usually called multimethods.

There is some run-time cost associated with the dynamic dispatch of function calls. In some languages the distinction between function overloading and multimethods can become blurred, with the compiler determining whether the choice of the function to call can be made at compile time or whether slower run-time dispatch will be required.

Practical Use

To assess how often multiple dispatch is used in practice, Muschevici and co-authors studied applications that use dynamic dispatch. They analyzed nine applications, mostly compilers, written in six different programming languages: Common Lisp Object System, Dylan, Cecil, MultiJava, Diesel, and Nice. The results show that from 13% to 32% of generic functions use dynamic typing of a single argument, while from 2.7% to 6.5% of functions use dynamic typing of multiple arguments. The remaining 65%-93% of generic functions have a single concrete method (are overloaded), and were therefore not considered to use dynamic typing of their arguments. In addition, the study reports that from 2% to 20% of generic functions had two concrete implementations, and 3%-6% had three. The proportion of functions with a larger number of concrete implementations declined rapidly.

Theory

The theory of languages with multimethods was first developed by Castagna and co-authors, by defining a model for overloaded functions with late binding. This provided the first formalization of the problem of covariance and contravariance in object-oriented programming languages and a solution to the binary method problem.

Example

To better understand the difference between multimethods and single dispatch, the following example can be used. Imagine a game in which, alongside various other objects, there are asteroids and spaceships. When any two objects collide, the program must choose a specific algorithm of action, depending on what collided with what.

Common Lisp

In a language that supports multimethods, such as Common Lisp, the code would look like this:

(defgeneric collide (x y))

(defmethod collide ((x asteroid) (y asteroid))
  ;;asteroid collides with asteroid
  )

(defmethod collide ((x asteroid) (y spaceship))
  ;;asteroid collides with spaceship
  )

(defmethod collide ((x spaceship) (y asteroid))
  ;;spaceship collides with asteroid
  )

(defmethod collide ((x spaceship) (y spaceship))
  ;;spaceship collides with spaceship
  )

and similarly for the other methods. Explicit checking and “dynamic type casting” are not used here.

With multiple dispatch available, the traditional approach of defining methods within classes and storing them in objects becomes less attractive, since each collide-with method belongs to two different classes rather than one. As a result, the special syntax for calling a method generally disappears, so that a method call looks exactly like an ordinary function call, and methods are grouped not by classes but into generic functions.

Raku

Raku, like its earlier versions, draws on proven ideas from other languages and type systems, offering compelling benefits for compiler-side code analysis and powerful semantics through multiple dispatch.

It has both multimethods and multi-subs. Since most operators are subroutines, there are also operators with multiple dispatch.

Alongside ordinary type constraints, it also has “where” constraints, which make it possible to create highly 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 doesn't make 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));

Python

In languages that do not support multiple dispatch at the syntax level, such as Python, it is generally possible to use multiple dispatch by means of extension libraries. For example, the multimethods.py module implements CLOS-style multimethods in Python without changing the language's syntax or keywords.

from multimethods import Dispatch
from game_objects import Asteroid, Spaceship
from game_behaviors import ASFunc, SSFunc, SAFunc
collide = Dispatch()
collide.add_rule((Asteroid,  Spaceship), ASFunc)
collide.add_rule((Spaceship, Spaceship), SSFunc)
collide.add_rule((Spaceship,  Asteroid), SAFunc)
def AAFunc(a, b):
    """Behavior when asteroid hits asteroid"""
    # ...define new behavior...
collide.add_rule((Asteroid, Asteroid), AAFunc)
# ...later...
collide(thing1, thing2)

Functionally this is very similar to the CLOS example, but the syntax follows standard Python syntax.

Using Python 2.4 decorators, Guido van Rossum wrote an example implementation of multimethods with simplified syntax:

@multimethod(Asteroid, Asteroid)
def collide(a, b):
    """Behavior when asteroid hits asteroid"""
    # ...define new behavior...
@multimethod(Asteroid, Spaceship)
def collide(a, b):
    """Behavior when asteroid hits spaceship"""
    # ...define new behavior...
# ... define other multimethod rules ...

and the multimethod decorator is defined further on.

The PEAK-Rules package implements multiple dispatch with syntax similar to the example shown above.

Emulating Multiple Dispatch

Java

In languages that have only single dispatch, such as Java, this code would look as follows (although the Visitor pattern can help solve this problem):

C

C has no dynamic dispatch, so it must be implemented manually in one form or another. An enumeration is often used to identify the subtype of an object. Dynamic dispatch can be implemented by looking up this value in a branch table of function pointers. Here is a simple example, in C:

typedef void (*CollisionCase)();

void collision_AA() { /* handle Asteroid-Asteroid collision */   };
void collision_AS() { /* handle Asteroid-Spaceship collision */  };
void collision_SA() { /* handle Spaceship-Asteroid collision */  };
void collision_SS() { /* handle Spaceship-Spaceship collision */ };

typedef enum {
    asteroid = 0,
    spaceship,
    num_thing_types /* not an object type itself; used to find the number of objects */
} Thing;

CollisionCase collisionCases[num_thing_types][num_thing_types] = {
    {&collision_AA, &collision_AS},
    {&collision_SA, &collision_SS}
};

void collide(Thing a, Thing b) {
    (*collisionCases[a][b])();
}

int main() {
    collide(spaceship, asteroid);
}

C++

As of 2015, C++ supports only single dispatch, although support for multiple dispatch has been considered. The ways of working around this limitation are similar: either using the Visitor pattern, or dynamic type casting:


 // Example using run time type comparison via dynamic_cast

 struct Thing {
     virtual void collideWith(Thing& other) = 0;
 };

 struct Asteroid : Thing {
     void collideWith(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 (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
         }
     }
 };

 struct Spaceship : Thing {
     void collideWith(Thing& 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 a lookup table of pointers to methods:


#include <unordered_map>
#include <typeinfo>

typedef unsigned uint4;
typedef unsigned long long uint8;

class Thing {
  protected:
    Thing(const uint4 cid) : tid(cid) {}
    const uint4 tid; // type id

    typedef void (Thing::*CollisionHandler)(Thing& other);
    typedef std::unordered_map<uint8, CollisionHandler> CollisionHandlerMap;

    static void addHandler(const uint4 id1, const uint4 id2, const CollisionHandler handler) {
        collisionCases.insert(CollisionHandlerMap::value_type(key(id1, id2), handler));
    }
    static uint8 key(const uint4 id1, const uint4 id2) {
        return uint8(id1) << 32 | id2;
    }

    static CollisionHandlerMap collisionCases;

  public:
    void collideWith(Thing& other) {
        CollisionHandlerMap::const_iterator 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 Thing {
    void asteroid_collision(Thing& other)   { /*handle Asteroid-Asteroid collision*/ }
    void spaceship_collision(Thing& other)  { /*handle Asteroid-Spaceship collision*/}

  public:
    Asteroid(): Thing(cid) {}
    static void initCases();
    static const uint4 cid;
};

class Spaceship: public Thing {
    void asteroid_collision(Thing& other)   { /*handle Spaceship-Asteroid collision*/}
    void spaceship_collision(Thing& other)  { /*handle Spaceship-Spaceship collision*/}

  public:
    Spaceship(): Thing(cid) {}
    static void initCases();
    static const uint4 cid; // class id
};

Thing::CollisionHandlerMap Thing::collisionCases;
const uint4 Asteroid::cid  = typeid(Asteroid).hash_code();
const uint4 Spaceship::cid = typeid(Spaceship).hash_code();

void Asteroid::initCases() {
    addHandler(cid, cid, (CollisionHandler) &Asteroid::asteroid_collision);
    addHandler(cid, Spaceship::cid, (CollisionHandler) &Asteroid::spaceship_collision);
}

void Spaceship::initCases() {
    addHandler(cid, Asteroid::cid, (CollisionHandler) &Spaceship::asteroid_collision);
    addHandler(cid, cid, (CollisionHandler) &Spaceship::spaceship_collision);
}

int main() {
    Asteroid::initCases();
    Spaceship::initCases();

    Asteroid  a1, a2;
    Spaceship s1, s2;

    a1.collideWith(a2);
    a1.collideWith(s1);

    s1.collideWith(s2);
    s1.collideWith(a1);
}

The yomm11 library makes it possible to automate this approach.

Stroustrup, Bjarne (1994). “Section 13.8”. ''The Design and Evolution of C++''. Indianapolis, IN, U.S.A: Addison Wesley. In his book “The Design and Evolution of C++”, Stroustrup mentions that he liked the concept of multimethods and that he considered implementing them in C++, but states that he was unable to find an example of an efficient implementation of them (compared to virtual functions) or to solve certain possible type-ambiguity problems. He further states that, although it would be nice to implement support for this concept, it can be approximated by double dispatch or a type-based lookup table, as described in the C/C++ example above, so this task has a low priority in the development of future versions of the language.

Implementation in Programming Languages

  • Common Lisp (via the Common Lisp Object System)
  • Haskell, via multi-parameter type classes
  • Scala, also via multi-parameter type classes
  • Elixir
  • Dylan
  • Nice
  • Cecil
  • R
  • Julia
  • Groovy
  • Lasso
  • Raku
  • Seed7
  • Clojure
  • C# 4.0
  • Fortress
  • TADS
  • Xtend
  • Nim

Support for multimethods in other languages via extensions:

  • Scheme (via TinyCLOS)
  • Python (via PEAK-Rules, RuleDispatch, gnosis.magic.multimethods, PyMultimethods, or multipledispatch)
  • Perl (via the Class::Multimethods module)
  • Java (via the MultiJava extension)
  • Ruby (via the Multiple Dispatch Library, the Multimethod and Vlx-Multimethods packages)
  • .NET (via the MultiMethods.NET library)
  • C# (via the multimethod-sharp library)
  • C++ (via the yomm11 library)
  • Factor (in the standard multi-methods vocabulary)

Multi-parameter type classes in Haskell and Scala can also be used to emulate multimethods.

See also

  • [[b14345]]
  • [[b14347]]
  • [[b14346]]

Comments

To leave a comment

If you have any suggestion, idea, thanks or comment, feel free to write. We really value feedback and are glad to hear your opinion.
To reply

Lectures and tutorial on "Object oriented programming"

Terms: Object oriented programming