Skip to content

Commit ccd273b

Browse files
committed
Merge pull request #51 from python-effect/foldE
add a `fold_effect` function
2 parents 598820d + 6b3e6d8 commit ccd273b

File tree

4 files changed

+131
-0
lines changed

4 files changed

+131
-0
lines changed

docs/source/api/effect.fold.rst

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
effect.fold module
2+
==================
3+
4+
.. automodule:: effect.fold
5+
:members:
6+
:undoc-members:
7+
:show-inheritance:

docs/source/api/effect.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ Submodules
88

99
effect.async
1010
effect.do
11+
effect.fold
1112
effect.ref
1213
effect.retry
1314
effect.testing

effect/fold.py

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
import traceback
2+
from functools import reduce
3+
4+
from effect import Constant, Effect
5+
6+
7+
class FoldError(Exception):
8+
"""
9+
Raised when one of the Effects passed to :func:`fold_effect` fails.
10+
11+
:ivar accumulator: The data accumulated so far, before the failing Effect.
12+
:ivar wrapped_exception: The exc_info tuple representing the original
13+
exception raised by the failing Effect.
14+
"""
15+
def __init__(self, accumulator, wrapped_exception):
16+
self.accumulator = accumulator
17+
self.wrapped_exception = wrapped_exception
18+
19+
def __str__(self):
20+
tb_lines = traceback.format_tb(self.wrapped_exception[2])
21+
tb = ''.join(tb_lines)
22+
return "FoldError(%r, %r) -> ORIGINAL TRACEBACK FOLLOWS\n%s" % (
23+
self.accumulator, self.wrapped_exception, tb)
24+
25+
26+
def fold_effect(f, initial, effects):
27+
"""
28+
Fold over the results of effects, left-to-right.
29+
30+
This is like :func:`functools.reduce`, but instead of acting on plain
31+
values, it acts on the results of effects.
32+
33+
The function ``f`` will be called with the accumulator (starting with
34+
``initial``) and a result of an effect repeatedly for each effect. The
35+
result of the previous call will be passed as the accumulator to the next
36+
call.
37+
38+
For example, the following code evaluates to an Effect of 6::
39+
40+
fold_effect(operator.add, 0, [Effect(Constant(1)),
41+
Effect(Constant(2)),
42+
Effect(Constant(3))])
43+
44+
If no elements were in the list, Effect would result in 0.
45+
46+
:param callable f: function of ``(accumulator, element) -> accumulator``
47+
:param initial: The value to be passed as the accumulator to the first
48+
invocation of ``f``.
49+
:param effects: sequence of Effects.
50+
"""
51+
52+
def failed(acc, e):
53+
raise FoldError(acc, e)
54+
55+
def folder(acc, element):
56+
return acc.on(lambda r: element.on(lambda r2: f(r, r2),
57+
error=lambda e: failed(r, e)))
58+
59+
return reduce(folder, effects, Effect(Constant(initial)))

effect/test_fold.py

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
2+
import operator
3+
4+
from pytest import raises
5+
6+
from effect import (
7+
ComposedDispatcher, Effect, Error,
8+
base_dispatcher, sync_perform)
9+
from effect.fold import FoldError, fold_effect
10+
from effect.testing import SequenceDispatcher
11+
12+
13+
def _base_and(dispatcher):
14+
"""Compose base_dispatcher onto the given dispatcher."""
15+
return ComposedDispatcher([dispatcher, base_dispatcher])
16+
17+
18+
def test_fold_effect():
19+
"""
20+
:func:`fold_effect` folds the given function over the results of the
21+
effects.
22+
"""
23+
effs = [Effect('a'), Effect('b'), Effect('c')]
24+
25+
dispatcher = SequenceDispatcher([
26+
('a', lambda i: 'Ei'),
27+
('b', lambda i: 'Bee'),
28+
('c', lambda i: 'Cee'),
29+
])
30+
eff = fold_effect(operator.add, 'Nil', effs)
31+
32+
with dispatcher.consume():
33+
result = sync_perform(_base_and(dispatcher), eff)
34+
assert result == 'NilEiBeeCee'
35+
36+
37+
def test_fold_effect_empty():
38+
"""
39+
Returns an Effect resulting in the initial value when there are no effects.
40+
"""
41+
eff = fold_effect(operator.add, 0, [])
42+
result = sync_perform(base_dispatcher, eff)
43+
assert result == 0
44+
45+
46+
def test_fold_effect_errors():
47+
"""
48+
When one of the effects in the folding list fails, a FoldError is raised
49+
with the accumulator so far.
50+
"""
51+
effs = [Effect('a'), Effect(Error(ZeroDivisionError('foo'))), Effect('c')]
52+
53+
dispatcher = SequenceDispatcher([
54+
('a', lambda i: 'Ei'),
55+
])
56+
57+
eff = fold_effect(operator.add, 'Nil', effs)
58+
59+
with dispatcher.consume():
60+
with raises(FoldError) as excinfo:
61+
sync_perform(_base_and(dispatcher), eff)
62+
assert excinfo.value.accumulator == 'NilEi'
63+
assert excinfo.value.wrapped_exception[0] is ZeroDivisionError
64+
assert str(excinfo.value.wrapped_exception[1]) == 'foo'

0 commit comments

Comments
 (0)