Slides at https://github.com/rbtcollins/reveal.js
Robert Collins
[email protected]
@rbtcollins (Twitter)
@radix
https://pypi.python.org/pypi/effect
https://github.com/python-effect/effect
note: code with predictable behaviours Fast/Easy testing of same code Reliable testing of same code (purity)
Whats wrong with this code?
print("What... is your quest?")
$ python ./01.py > /dev/full
Exception ignored in: <_io.TextIOWrapper name='<stdout>' mode='w' encoding='UTF-8'>
OSError: [Errno 28] No space left on device
$ echo $?
0
http://bugs.python.org/issue5319
global state
- name lookup: print
- marshall args
- call it
- name lookup: sys.stdout
- .write
- output buffered
note:
- global
- local
- local
- global
- local
- global
- Monkeypatching
- Subprocesses
- IO Redirection
How might we fix this?
import sys
print("What... is your quest?", file=sys.stdout)
sys.stdout.flush()
Traceback (most recent call last):
File "./02.py", line 3, in <module>
sys.stdout.flush()
OSError: [Errno 28] No space left on device
Exception ignored in: <_io.TextIOWrapper name='<stdout>' mode='w' encoding='UTF-8'>
OSError: [Errno 28] No space left on device
echo $?
1
Testing is still undesirable
note: Terms to solve problems we do not have.
note: such as..
import sys
fred = 1
sys.modules['__main__'].fred = 2
print("%s" % fred)
Note: We know in Python, but Haskell is pure math: similar code in haskell is not an instruction to do something, its a statement about how to derive a value if the value is needed.
do x1 <- action1
x2 <- action2
action3 x1 x2
action1 >>=
\ x1 -> action2 >>=
\ x2 -> action3 x1 x2
>>= is an infix function called bind.
def _1(x1):
def _2(x2):
return action3(x1, x2)
return _2(action2())
result = _1(action1())
Beep, wrong
>>= is polymorphic on its left hand argument. We are missing the monad.
class Monad:
def __init__(self, v):
self.v = v
def bind(self, f):
return f(self, self.v)
def unit(self, v):
return self.__class__(v)
def action1(m): return m.unit(1)
def action2(m): return m.unit(2)
def action3(m, x, y): return m.unit(x+y)
def _1(m1, x1):
def _2(m2, x2):
return action3(m2, x1, x2)
return action2(m1).bind(_2)
m = Monad(None)
lastline = action1(m).bind(_1)
print(lastline.v)
- actions can do anything
- stacks of lambdas are hard to reason about
http://www.haskellforall.com/2012/07/purify-code-using-free-monads.html
approximated in Python by @radix
note: Chris had read that and it may or may not be part of the inspiration for Effect.
note: The name makes sense but the explanation is longer than this talk.
class Print:
def __init__(self, line):
self.line = line
def program():
return Effect(Print('What... is your quest?'))
- Return generators or Effects
- Functions accept a single parameter
@sync_performer
def real_print(dispatcher, print_):
print(print_.line)
sys.stdout.flush()
real_interpreter = ComposedDispatcher([
TypeDispatcher({
Print: real_print,
}),
base_dispatcher])
note: We have split out business logic and things that must be impure
def test_print(self):
outputs = []
@sync_performer
def perform_test(dispatcher, print_):
outputs.append(print_.line)
Cannot assert here in the general case.
test_interpreter = ComposedDispatcher([
TypeDispatcher({
Print: perform_print,
}),
base_dispatcher])
dispatcher = test_interpreter
sync_perform(dispatcher, program())
self.assertEqual(["What... is your quest?"], outputs)
MonkeypatchingSubprocessessIO Redirection
Little awkward with closures etc.
note: Arguably a design defect (see the free monad blog post in haskell) - being able to pass a RealWorld equivalent in would allow each test interpeter to be independent without closures.
effect.testing.SequenceDispatcher
def test_print(self):
sequence = SequenceDispatcher([
(Print('What... is your quest?'), lambda _:None),
])
with sequence.consume():
sync_perform(sequence, program())
Print = namedtuple("Print", "line")
note: Named tuples because SequenceDispatcher uses equality.
Readline = namedtuple("Readline", "")
@sync_performer
def real_readline(dispatcher, readline):
return sys.stdin.readline()
real_interpreter = ComposedDispatcher([
TypeDispatcher({
Print: real_print,
Readline: real_readline,
}),
base_dispatcher])
def echo():
result = Effect(Print('What... is your quest?')).on(
success=lambda _: Effect(Readline())).on(
success=lambda r: Effect(Print(r)))
return result
@given(st.text())
def test_echo(self, line):
sequence = SequenceDispatcher([
(Print('What... is your quest?'), lambda _:None),
(Readline(), lambda _:line),
(Print(line), lambda _:None),
])
with sequence.consume():
sync_perform(sequence, echo())
from effect.do import do
...
@do
def echo():
yield Effect(Print('What... is your quest?'))
line = yield Effect(Readline())
yield Effect(Print(line))
with sequence.consume():
dispatcher = ComposedDispatcher([
sequence,
base_dispatcher,
])
sync_perform(dispatcher, echo())
@do
def challenge():
line = None
while line != 'To seek the Holy Grail.\n':
yield Effect(Print('What... is your quest?'))
line = yield Effect(Readline())
yield Effect(Print('What... is your favourite colour?'))
@given(st.text())
def test_challenge(self):
sequence = SequenceDispatcher([
(Print('What... is your quest?'), lambda _:None),
(Readline(), lambda _: line),
(Print('What... is your quest?'), lambda _:None),
(Readline(), lambda _:'To seek the Holy Grail.\n'),
(Print('What... is your favourite colour?'), lambda _:None),
])
from effect.do import do_return
@do
def challenge():
line = None
while line != 'To seek the Holy Grail.\n':
yield Effect(Print('What... is your quest?'))
line = yield Effect(Readline())
yield do_return(line)
@given(st.text())
def test_challenge(self):
sequence = [
(Print('What... is your quest?'), lambda _:None),
(Readline(), lambda _: line),
(Print('What... is your quest?'), lambda _:None),
(Readline(), lambda _:'To seek the Holy Grail.\n'),
]
result = perform_sequence(sequence, challenge())
self.assertEqual(result, 'To seek the Holy Grail.\n')
Example code: https://github.com/rbtcollins/reveal.js/tree/master/effectsnippets
- Robert Collins
- @rbtcollins
- lifeless on freenode
- [email protected]