Fosdem 02/01/2020
Vincent Perez
The goals of this talk are to show:
Monads are known to be a scary concept, but..
Warning: Will rely more on examples than theory
Pb: Timed functions
from functools import wraps
from time import sleep, time
# Decorator to time function execution
def time_it(f):
    @wraps(f)
    def wrapper(*args, **kwargs):
        start = time()
        result = f(*args, **kwargs)
        end = time()
        return result, end - start
    return wrapperPb: Timed functions
# let's define some functions
@time_it
def fast(x):
    return x + 1
@time_it
def slow(x):
    sleep(0.1)
    return x + 1
@time_it
def slow2(x):
    sleep(0.1)
    return x + 2How to chain these and get the total time?
Pb: Timed functions
# Method 1: the obvious way
x0, time0 = fast(1)
x1, time1 = slow(x0)
x2, time2 = slow2(x1)
total_time = time0 + time1 + time2How to chain these and get the total time?
Pb: Timed functions
What could be improved here?
x0, time0 = fast(1)
x1, time1 = slow(x0)
x2, time2 = slow2(x1)Pb: Timed functions
What could be improved ?
total_time = time0 + time1 + time2Pb: Timed functions (with bind)
def bind(value_and_time, f):
    """
    :param value_and_time: Tuple[T, float]
    :param f: Callable[[T], Tuple[U, float]]
    :rtype: Tuple[U, float]
    """
    result, t = f(value_and_time[0])
    return result, t + value_and_time[1]
x2, t = bind(bind(fast(1), slow), slow2)Pb: Timed functions (with bind)
def bind(value_and_time, f):
    return result, t + value_and_time[1]
x2, t = bind(bind(fast(1), slow), slow2)
Pb: Timed functions (with bind + object notation)
class TimedValue(object):
    def __init__(self, value, time=0):
        self.value = value
        self.time = time
    def bind(self, f):
        timed_value = f(self.value)
        new_value = timed_value.value
        new_time = self.time + timed_value.time
        return TimedValue(new_value,  new_time)Pb: Timed functions (with bind + object notation)
class TimedValue(object):    
    ...     
    def bind(self, f):
        timed_value = f(self.value)
        new_value = timed_value.value
        new_time = self.time + timed_value.time
        return TimedValue(new_value, new_time)
# Keep same functions as before, but change the decorator
def time_it(f):
    @wraps(f)
    def wrapper(*args, **kwargs):
        start = time()
        result = f(*args, **kwargs)
        end = time()
        return TimedValue(result, end - start)
    return wrapperPb: Timed functions (with bind + object notation)
timed_value = (
    fast(1)
    .bind(slow)
    .bind(slow2)
)
value = timed_value.value
time = timed_value.timeSame as before, but chaining methods instead of nesting functions.
What did we just do ?
We just invented a monad (the TimedValue class)!
Our friend the monad
What is a monad?
Our friend the monad
What is it good for?
Our friend the monad, formal introduction
How to define a monad:
Our friend the monad
class TimedValue(object):
    def __init__(self, value, time=0):
        self.value = value
        self.time = time
    @classmethod
    def unit(cls, value):
        return cls(value)Our friend the monad
class TimedValue(object):
    def __init__(self, value, time=0):
        self.value = value
        self.time = time
    def bind(self, f):
        timed_value = f(self.value)
        new_value = timed_value.value
        new_time = self.time + timed_value.time
        return TimedValue(new_value,  new_time)Another example: the Maybe monad
user = props.user
friends = user.friends if user else None
first_friend = friends[0] if len(friends) > 0 else None
friends_of_first_friend = (
  first_friend.friends if first_friend else None
)Another example: the Maybe monad
Maybe Monad: rationale
Another example: the Maybe monad
class Maybe(object):
    def __init__(self, value):
        self.value = value
    @classmethod
    def unit(cls, value):
        return cls(value)
     def bind(self, f):
        if self.value is None:
            return self  # forward the empty value          
        
        result = f(self.value)        
        if isinstance(result, Maybe):
            return result
        else:
            # note to monad experts: conflating bind and map
            return Maybe.unit(result)Another example: the Maybe monad
def first_value(values):
    if len(values) > 0:
        return values[0]
    return None
friends_of_first_friends = (
    Maybe.unit(props)
    .bind(lambda props: props.user)
    .bind(lambda user: user.friends)
    .bind(first_value)
    .bind(lambda first_friend: first_friend.friends)
)Another example: the Maybe monad
# instead of writing this:
Maybe.unit(obj).bind(lambda obj: obj.method())
# we would like to write
Maybe.unit(obj).method()
# we can probably hack something with __getattr__ ...Another example: the Maybe monad
Text
def __getattr__(self, name):
        field = getattr(self.value, name)
        if not callable(field):
            return self.bind(lambda _: field)
        return lambda *args, **kwargs: self.bind(
            lambda _: field(*args, **kwargs)
        )
Another example: the Maybe monad
def first_value(values):
    if len(values) > 0:
        return values[0]
    return None
friends_of_first_friends = (
    Maybe.unit(props)
    .user
    .friends
    .bind(first_value)
    .friends
)
Monad comprehensions
Let's reconsider our TimedValue example
x0, time0 = fast(1)
x1, time1 = slow(x0)
x2, time2 = slow2(x1)
total_time = time0 + time1 + time2What if we don't have a linear pipeline? (eg the final value depends on x0)
final_value = x0 * x2Monad comprehensions
Let's reconsider our TimedValue example , monadic version
timed_value = (
    fast(1)
    .bind(
        lambda x0: slow(x1)
        .bind(slow2)
        .bind(lambda x2: x0 * x2)
    )
)
final_value = timed_value.value
time = timed_value.timeCan we have a friendlier syntax ?
Monad comprehensions
What we would like to write is this:
timed_value = [
  x0 * x2
  for x0 in fast(1)
  for x1 in slow(x1)
  for x2 in slow2(x2)
]
final_value = timed_value.value
time = timed_value.timeMonad comprehensions
Monad comprehensions
Python source code
AST
Python Bytecode
Scan + Parse
Compile
Interpret
NewAst
Ast Transform
Compile
CPython program execution and AST Transformation
Monad comprehensions
Ast transformations with a decorator
def monad_comprehension(monad_cls):
	"""  
    :param monad_cls: A class implementing
    `unit` and `bind`
    """
    def decorator(f):
        """
        :param f: function in which we
        want to overload list comprehensions
        """
        # ast transformations ...Monad comprehensions
Demo #1
# AST transformations wrapped into a function decorator
@monad_comprehension(TimedValue)
def f():
    return [
        x0 * x2
        for x0 in fast(1)
        for x1 in slow(x0)
        for x2 in slow2(x1)
    ]
  Monad comprehensions
Demo #2
# AST transformations wrapped into a function decorator
@monad_comprehension(Maybe)
def f():
    return [
        (x + y)
        for x in Maybe(5)
        for y in Maybe(6)
    ]
# outputs `Just 11`Monad comprehensions
Demo #3
# AST transformations wrapped into a function decorator
@monad_comprehension(Maybe)
def f():
    return [
        (x + y)
        for x in Maybe(None)
        for y in Maybe(6)
    ]
# outputs the empty valueWe can now easily handle non linear pipelines \o/
Conclusion
Take aways
Conclusion
References (Thanks to these guys):
Conclusion
Find the code on github:
https://github.com/v-prz/monad_comprehension.py
https://vincent-prz.github.io/
https://github.com/vincent-prz