Callbacks and Continuations#

Don’t call me. I’ll call you.

import threading

def long_running(callback):
    def done():
        result = 42
        callback(result)
    timer = threading.Timer(5.0, done)
    timer.start()

long_running(print)

Continuation Passing Style (CPS)#

This is a functional programming style where you don’t return any values from your functions. Instead of returning the result, you pass a continuation function that will be applied to the result.

import math

def add(a, b):
    return a + b

def square(x):
    return x * x

def sqrt(x):
    return math.sqrt(x)

def pythagoras(a, b):
    return sqrt(add(square(a), square(b)))
result = pythagoras(2,3)
print(result)
import math

def add(a, b, cont):
    cont(a + b)

def square(x, cont):
    cont(x * x)

def sqrt(x, cont):
    cont(math.sqrt(x))

# Pythagoras rewritten in CPS
def pythagoras(a, b, cont):
    square(a, (lambda aa:
        square(b, (lambda bb:
            add(aa, bb, (lambda aabb:
                sqrt(aabb, (lambda result:
                    cont(result)
                ))
            ))
        ))
    ))
pythagoras(2, 3, print)

Nice, but unreadable. Kind of ….#

How can we do better? We’re not really used to pass in continuations. We like to return our results!

Could we perhaps use currying?

So instead of taking a continuation, we could return a function that takes a continuation. It’s basically the exact same thing … just moving the :.

import math

def add(a, b):
    return lambda cont: cont(a + b)

def square(x):
    return lambda cont: cont(x * x)

def sqrt(x):
    return lambda cont: cont(math.sqrt(x))

def pythagoras(a, b):
    def then(cont):
        then = square(a)
        def next(aa):
            then = square(b)
            def next(bb):
                then = add(aa, bb)
                def next(aabb):
                    then = sqrt(aabb)
                    def next(result):
                        cont(result)
                    then(next)
                then(next)
            then(next)
        then(next)
    return then

result = pythagoras(2,3)
result(print)

Now what? Looks slightly better, kind of …#

Could we perhaps use types to make better abstractions?

In Python we create new types using classes, so let’s create a class to encapsulte the CPS function (a -> r) -> r.

Ref: https://wiki.haskell.org/MonadCont_under_the_hood

class Cont:
    def __init__(self, cps):
        self.__cps = cps # fn: ('a -> 'r) -> 'r

    @staticmethod
    def rtn(a):
        return Cont(lambda cont: cont(a))

    def run(self, cont):
        self.__cps(cont)

    def then(self, fn):
        # Cont <| fun c -> run cont (fun a -> run (fn a) c )
        return Cont(lambda c: self.run(lambda a: fn(a).run(c)))
import math

def add(a, b):
    return Cont.rtn(a + b)

def square(x):
    return Cont.rtn(x * x)

def sqrt(x):
    return Cont.rtn(math.sqrt(x))

def pythagoras(a, b):
    return square(a).then(
        lambda aa: square(b).then(
            lambda bb: add(aa, bb).then(
                lambda aabb: sqrt(aabb)
            )
        )
    )
result = pythagoras(2, 3)
result.run(print)
import asyncio

class Cont:
    def __init__(self, cps):
        self.__cps = cps # fn: ('a -> 'r) -> 'r

    @staticmethod
    def rtn(a):
        return Cont(lambda cont: cont(a))

    def run(self, cont):
        self.__cps(cont)

    def __await__(self):
        loop = asyncio.get_event_loop()
        future = loop.create_future()
        def done(value):
            future.set_result(value)
        self.run(done)
        return iter(future)

    def then(self, fn):
        # Cont <| fun c -> run cont (fun a -> run (fn a) c )
        return Cont(lambda c: self.run(lambda a: (fn(a).run(c))))
import math

def add(a, b):
    return Cont.rtn(a + b)

def square(x):
    return Cont.rtn(x * x)

def sqrt(x):
    return Cont.rtn(math.sqrt(x))

async def pythagoras(a, b):
    aa = await square(a)
    bb = await square(b)
    aabb = await add(aa, bb)
    ab = await sqrt(aabb)

    return ab

result = await pythagoras(2,3)
print(result)

Conclusion#

Async / await is basically just syntactic sugar for working with effects such as callbacks and continuations

How do we know that the “normal” syntax of a programming language is not compiled to continuations under the hood? Maybe we have been programming with continuations all along?

The Mother of all Monads#

from typing_extensions import Protocol, runtime_checkable
from abc import abstractmethod

@runtime_checkable
class Monad(Protocol):
    @staticmethod
    @abstractmethod
    def rtn(a):
        raise NotImplementedError

    @abstractmethod
    def then(self, fn):
        raise NotImplementedError
assert issubclass(Cont, Monad)
print("Yey!!")