Multiple Dispatch

Single Dispatch

Single Dispatch

  • Code reuse via inheritance
  • Code works with many different types
  • Late binding
  • Goes by a few names
    • Virtual functions
    • Dynamic dispatch
    • Probably others
  • Python: everything is virtual
  • Not free
  • Exercise: implement virtual functions in pure C

Example

class Animal:
    def speak(self):
        raise NotImplementedError()


class Dog(Animal):
    def speak(self):
        print('Woof!')


class Cat(Animal):
    def speak(self):
        print('Meow!')


def make_some_noise(animal: Animal):
    animal.speak()


make_some_noise(Dog())
make_some_noise(Cat())

Bad Example

class Dog:
    def woof(self):
        print('Woof!')


class Cat:
    def meow(self):
        print('Meow!')


def make_some_noise(animal):
    if isinstance(animal, Dog):
        animal.woof()
    elif isinstance(animal, Cat):
        animal.meow()


make_some_noise(Dog())
make_some_noise(Cat())

Bad Example

class Bird:
    def tweet(self):
        print('Tweet!')


# Now go back and add another isinstance check

Multiple Dispatch

Naive Implementation

  • Store a mapping of declared argument types to function
  • Lookup function based on argument types when called

Naive Implementation

>>> @naive_dispatch(int, int)
>>> def add(a, b):
...     return a + b
...
>>> add(1, 2)
3
>>> class MyInt(int):
...     ...
...
>>> add(MyInt(1), 2)
KeyError: Your implementation is naive

Generalization of Single Dispatch

Look at every argument

  • Single dispatch is the first argument or implied
  • Instead, choose the function to call based on RTT of every argument

Why even care?

  • More natural for certain classes of operations
  • Enables overriding behavior without inheritance
  • Very clean recursion/delegation
  • Parsimonious user-facing APIs
  • Libraries become a lot more externally extensible
  • Case study: Julia

Example: JavaScript Addition

A Trustworthy Person

Example

from multipledispatch import dispatch


@dispatch(int, int)
@dispatch(float, int)
@dispatch(int, float)
@dispatch(float, float)
def javascript_add(a, b):
    return float(a) + float(b)


@dispatch(str, int)
@dispatch(int, str)
def javascript_add(a, b):
    return str(a) + str(b)


@dispatch(str, float)
def javascript_add(a, b):
    if b.is_integer():
        return a + str(int(b))
    return a + str(b)


@dispatch(float, str):
def javascript_add(a, b):
    if a.is_integer():
        return str(int(a)) + b
    return str(a) + b
>>> javascript_add(1, 2)
3.0
>>> javascript_add(1.0, 2)
3.0
>>> javascript_add(1, 'a')
'1a'
>>> javascript_add('a', 1.0)
'a1'

Example

from multipledispatch import dispatch


@dispatch(int, int)
@dispatch(float, int)
@dispatch(int, float)
@dispatch(float, float)
def javascript_add(a, b):
    return float(a) + float(b)


@dispatch(str, int)
@dispatch(int, str)
def javascript_add(a, b):
    return str(a) + str(b)


@dispatch(str, float)
def javascript_add(a, b):
    if b.is_integer():
        return a + str(int(b))
    return a + str(b)


@dispatch(float, str):
def javascript_add(a, b):
    if a.is_integer():
        return str(int(a)) + b
    return str(a) + b
>>> javascript_add(1, 2)
3.0
>>> javascript_add(1.0, 2)
3.0
>>> javascript_add(1, 'a')
'1a'
>>> javascript_add('a', 1.0)
'a1'

Implementation

  • Graph algorithm
  • Nodes: function signatures
  • Edges: a -> b iff a is more specific than b (supercedes)
  • Topologically sort the graph
  • Call each function until we succeed or run out of things to call
  • Last year an intern implemented variadic dispatch

Pandas Backend

Heavy use of MD

How?

  • Define rules for each ibis operation
  • Rules define the procedure for evaluating an op

Cast

@execute_node.register(ops.Cast, type(None), dt.DataType)
def execute_cast_null_to_anything(op, data, type, **kwargs):
    return None


@execute_node.register(ops.Cast, datetime.datetime, dt.String)
def execute_cast_datetime_or_timestamp_to_string(op, data, type, **kwargs):
    """Cast timestamps to strings"""
    return str(data)


@execute_node.register(ops.Cast, datetime.datetime, dt.Int64)
def execute_cast_datetime_to_integer(op, data, type, **kwargs):
    """Cast datetimes to integers"""
    return pd.Timestamp(data).value


@execute_node.register(ops.Cast, pd.Timestamp, dt.Int64)
def execute_cast_timestamp_to_integer(op, data, type, **kwargs):
    """Cast timestamps to integers"""
    return data.value