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.
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.
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.
interact(yennefer, monster)
, what happens? What do you need to do?interact(witcher, witcher)
, what happens? How, can you make that a bit more robust?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