Pythonic monads
Fosdem 02/01/2020
Vincent Perez
- Vincent Perez, Software engineer
- ~ 4 years exp in Python
- Big fan of functional programming
The goals of this talk are to show:
- 
Monad is a simple (but powerful!) concept
 
- Monads can be leveraged in Python
Monads are known to be a scary concept, but..
Warning: Will rely more on examples than theory
Motivating example
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?
- Need to repeat the logic of unpacking value + time
x0, time0 = fast(1)
x1, time1 = slow(x0)
x2, time2 = slow2(x1)Pb: Timed functions
What could be improved ?
- Need to repeat summation logic
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)- value & time unboxing / time summation are encoded once in the bind function :)
- Nested binds can be a bit harder to read
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)!
But... what is it ?
Our friend the monad
What is a monad?
- Monad is a general concept (with TimedValue being one instance of this concept)
- It can be seen as a design pattern to help us compose functions with "effects"
- Analogy: monadic value -> amplified / boxed value / value with context (eg timed values, list of values...)
Our friend the monad
What is it good for?
- Make composition of functions easier
 
- Avoid repeating computational patterns
 
- Particularly useful when pipelining operations
 
Our friend the monad, formal introduction
How to define a monad:
- define a unit function (or constructor): plain value -> monadic value
- define a bind function: for applying a function to a monadic value
- unit and bind must respect some properties (not covered here)
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
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
)- Repeating the if ... else ... none guard => can we abstract this away with a monad ?
Another example: the Maybe monad
Maybe Monad: rationale
- two kind of values: full or empty
 
- if an empty value is encountered during a computation pipeline, just forward it
- This allows us not to check for `None` at every step
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)
)- We can chain functions without None guards (done once in bind) :)
- Can we be more concise?
- Yes, by leveraging the dynamism of Python!
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
)
Comprehending Monads (Experimental)
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.time- timed_value is a TimedValue! (not a list)
- for x0 in fast(1) reads as x0 = fast(1).value
Monad comprehensions
- This intuition comes from the fact that lists are monads, and a comprehension can be written in terms of List.unit and List.bind
- Idea: generalize comprehensions to all monads
- But Python doesn't allow us to overload the meaning of comprehensions!
- Solution: ast transformations!
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`- f returns a Maybe! (not a list)
- 
for x in Maybe(5) reads as
 x = Maybe(5).value
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
Conclusion
Take aways
- Monad is a simple concept
- (Simple) monads are simple to implement
- Not as alien as one may think
	- Deferred in twisted are a kind of monad
- Maybe monads equivalents are found in Rust, Java ...
- Promises: monad for async programming (Javascript)
- LINQ query syntax is very close to Monad comprehensions (C#)
 
Conclusion
References (Thanks to these guys):
- 
Dan Piponi:
	- http://blog.sigfpe.com/2012/03/overloading-python-list-comprehension.html
- http://blog.sigfpe.com/2006/08/you-could-have-invented-monads-and.html
 
- 
wesdyer
	- https://blogs.msdn.microsoft.com/wesdyer/2008/01/10/the-marvels-of-monads/
 
- 
Alexander Schepanovski
	- http://hackflow.com/blog/2015/03/29/metaprogramming-beyond-decency/
 
Conclusion
Find the code on github:
https://github.com/v-prz/monad_comprehension.py
Thank you!
https://vincent-prz.github.io/
https://github.com/vincent-prz
Pythonic monads
By v-perez
Pythonic monads
- 1,224
 
   
   
  