Multiple Dispatch

Multipledispatch 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.

Yeah, ok, thanks for the wikipedia quote Antonio. How am I supposed to know how to use that then? Hold your horses. Why don't we take a look at a practical application.

Say you work in gaming and you would like to create a bunch of interactions between two characters. Let's use the Witcher as an example. And if you haven't watched it yet, well here's your chance. If you haven't played it here's your chance to do that too. [Disclaimer] I am not suggesting that this is the pattern used in the game. It is merely used as an application example of the multiple dispatch pattern.

Les Personnages

Let's use a bunch of characters for fun with some defaults. Let's create a new file interactions.py and add the below code to it.

from dataclasses import dataclass

@dataclass
class AbstractCharacter:
    pass


@dataclass
class Witcher(AbstractCharacter):
    name: str = "Geralt of Rivia"


@dataclass
class Monster(AbstractCharacter):
    name: str = "Ghoul"


@dataclass
class Yennefer(AbstractCharacter):
    name: str = "Yennefer of Vengerberg"


@dataclass
class Bard(AbstractCharacter):
    name: str = "Dandelion"
interactions.py

Les Actions

When the above characters meet, they will perform one of the below actions. Now, mind you this is a simple example that can very easily explode if there are additional conditions in the equation. Let's be simple and we can go deeper in another blog.

In interactions.py let's also add the below. If you don't know what an Enum is, it will make sense later on.

from enum import Enum

class Action(str, Enum):
    FIGHT = "FIGHT"
    KISS = "KISS"
    SING = "SING"
interactions.py
Exercise 1: Format interactions.py with a linter of your choice.
Tip: isort, flake8, black

Les Interactions

Install multipledispatch like this: pip install multipledispatch, and add the below code to your interactions.py

from multipledispatch import dispatch

@dispatch(AbstractCharacter, AbstractCharacter)
def interact(character_a: AbstractCharacter, character_b: AbstractCharacter):
    if type(character_a) == type(character_b):
        raise ValueError("You screwed up and met yourself.")


@dispatch(Witcher, Monster)
def interact(witcher: Witcher, monster: Monster) -> Action:
    action = Action.FIGHT
    print(f"{witcher.name} will {action.name} the {monster.name}")
    return action


@dispatch(Witcher, Yennefer)
def interact(witcher: Witcher, yennefer: Yennefer) -> Action:
    action = Action.KISS
    print(f"{witcher.name} will {action.name} {yennefer.name}")
    return action


@dispatch(Witcher, Bard)
def interact(witcher: Witcher, bard: Bard):
    action = Action.SING
    print(f"{witcher.name} will {action.name} with {bard.name}")
    return action

Each interact function is wrapped inside a @dispatch decorator that does all the magic. The first interact function is taking care of the case where a character meets themselves (although totally possible in the magical realm of the Witcher, in our case we choose this approach as an example of dealing with an interaction that is not allowed).

After starting an interactive python session with python -i -m interactions, initialize the characters with the below:

witcher = Witcher()
yennefer = Yennefer()
ghoul = Monster()
dandelion = Bard()

And now:

>>> interact(witcher, monster)
Geralt of Rivia will FIGHT the Ghoul
<Action.FIGHT: 'FIGHT'>
>>> interact(witcher, yennefer)
Geralt of Rivia will KISS Yennefer of Vengerberg
<Action.KISS: 'KISS'>
>>> interact(witcher, dandelion)
Geralt of Rivia will SING with Dandelion
<Action.SING: 'SING'>

See? That was fun. A single function changes it's output depending the argument types.

Exercise 2: Execute interact(yennefer, monster), what happens? What do you need to do?
Exercise 3: Execute interact(witcher, witcher), what happens? How, can you make that a bit more robust?
Exercise 4: Execute interact(yennefer, monster), what's missing?

A bientôt!
Antoine


Reading Materials


Multiple Dispatch: multiple-dispatch.readthedocs.io
Single Dispatch: functools.singledispatch
Dictionary Dispatch: dictionary-dispatch