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 wrapper
Pb: 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 + 2
How 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 + time2
How 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 + time2
Pb: 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 wrapper
Pb: Timed functions (with bind + object notation)
timed_value = (
fast(1)
.bind(slow)
.bind(slow2)
)
value = timed_value.value
time = timed_value.time
Same 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 + time2
What if we don't have a linear pipeline? (eg the final value depends on x0)
final_value = x0 * x2
Monad 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.time
Can 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
Monad 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 value
We 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