Skip to content

Commit f589867

Browse files
WIP
1 parent 119b081 commit f589867

File tree

11 files changed

+430
-0
lines changed

11 files changed

+430
-0
lines changed

docs/index.rst

+1
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ Contents
4040
pages/functions.rst
4141
pages/curry.rst
4242
pages/types.rst
43+
pages/transducers.rst
4344

4445
.. toctree::
4546
:maxdepth: 2

docs/pages/transducers.rst

+14
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
.. transducers:
2+
3+
Transducers
4+
===========
5+
6+
API Reference
7+
-------------
8+
9+
.. automodule:: returns.transducers.transducers
10+
:members:
11+
12+
.. autofunction:: returns.transducers.tmap
13+
14+
.. autofunction:: returns.transducers.tfilter

returns/transducers/__init__.py

+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
from returns.transducers.tfilter import tfilter as tfilter
2+
from returns.transducers.tmap import tmap as tmap
3+
from returns.transducers.transducers import Missing as Missing
4+
from returns.transducers.transducers import Reduced as Reduced
5+
from returns.transducers.transducers import transduce as transduce
6+
from returns.transducers.transducers import treduce as treduce

returns/transducers/tfilter.py

+41
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
from typing import Callable, TypeVar
2+
3+
_ValueType = TypeVar('_ValueType')
4+
_AccValueType = TypeVar('_AccValueType')
5+
6+
7+
def tfilter(
8+
predicate: Callable[[_ValueType], bool],
9+
) -> Callable[
10+
[Callable[[_AccValueType, _ValueType], _AccValueType]],
11+
Callable[[_AccValueType, _ValueType], _AccValueType],
12+
]:
13+
"""
14+
:py:func:`filter <filter>` implementation on a transducer form.
15+
16+
.. code:: python
17+
18+
>>> from typing import List
19+
>>> from returns.transducers import tfilter, treduce
20+
21+
>>> def is_even(number: int) -> bool:
22+
... return number % 2 == 0
23+
24+
>>> def append(collection: List[int], item: int) -> List[int]:
25+
... collection.append(item)
26+
... return collection
27+
28+
>>> my_list = [0, 1, 2, 3, 4, 5, 6]
29+
>>> xform = tfilter(is_even)(append)
30+
>>> assert treduce(xform, my_list, []) == [0, 2, 4, 6]
31+
32+
"""
33+
def reducer(
34+
step: Callable[[_AccValueType, _ValueType], _AccValueType],
35+
) -> Callable[[_AccValueType, _ValueType], _AccValueType]:
36+
def filter_(acc: _AccValueType, value: _ValueType) -> _AccValueType:
37+
if predicate(value):
38+
return step(acc, value)
39+
return acc
40+
return filter_
41+
return reducer

returns/transducers/tmap.py

+41
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
from typing import Callable, TypeVar
2+
3+
_ValueType = TypeVar('_ValueType')
4+
_NewValueType = TypeVar('_NewValueType')
5+
6+
_AccValueType = TypeVar('_AccValueType')
7+
8+
9+
def tmap(
10+
function: Callable[[_ValueType], _NewValueType],
11+
) -> Callable[
12+
[Callable[[_AccValueType, _NewValueType], _AccValueType]],
13+
Callable[[_AccValueType, _ValueType], _AccValueType],
14+
]:
15+
"""
16+
A map implementation on a transducer form.
17+
18+
.. code:: python
19+
20+
>>> from typing import List
21+
>>> from returns.transducers import tmap, treduce
22+
23+
>>> def add_one(number: int) -> int:
24+
... return number + 1
25+
26+
>>> def append(collection: List[int], item: int) -> List[int]:
27+
... collection.append(item)
28+
... return collection
29+
30+
>>> my_list = [0, 1]
31+
>>> xformaa = tmap(add_one)(append)
32+
>>> assert treduce(xformaa, my_list, []) == [1, 2]
33+
34+
"""
35+
def reducer(
36+
step: Callable[[_AccValueType, _NewValueType], _AccValueType],
37+
) -> Callable[[_AccValueType, _ValueType], _AccValueType]:
38+
def map_(acc: _AccValueType, value: _ValueType) -> _AccValueType:
39+
return step(acc, function(value))
40+
return map_
41+
return reducer

returns/transducers/transducers.py

+158
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
1+
from typing import (
2+
Any,
3+
Callable,
4+
Generic,
5+
Iterable,
6+
Optional,
7+
TypeVar,
8+
final,
9+
overload,
10+
)
11+
12+
from returns.primitives.types import Immutable
13+
14+
_ValueType = TypeVar('_ValueType')
15+
_NewValueType = TypeVar('_NewValueType')
16+
17+
_AccValueType = TypeVar('_AccValueType')
18+
19+
20+
@final
21+
class Reduced(Immutable, Generic[_ValueType]):
22+
"""
23+
Sentinel for early termination inside transducer.
24+
25+
.. code:: python
26+
27+
>>> from returns.transducers import tmap, transduce, Reduced
28+
29+
>>> def add_one(number: int) -> int:
30+
... return number + 1
31+
32+
>>> def add(acc: int, number: int) -> int:
33+
... if acc == 3:
34+
... return Reduced(acc)
35+
... return acc + number
36+
37+
>>> my_list = [0, 1, 2]
38+
>>> assert transduce(tmap(add_one), add, 0, my_list) == 3
39+
40+
"""
41+
42+
__slots__ = ('_inner_value',)
43+
44+
_inner_value: _ValueType
45+
46+
def __init__(self, inner_value: _ValueType) -> None:
47+
"""Encapsulates the value from early reduce termination."""
48+
object.__setattr__(self, '_inner_value', inner_value) # noqa: WPS609
49+
50+
@property
51+
def value(self) -> _ValueType: # noqa: WPS110
52+
"""Returns the value from early reduce termination."""
53+
return self._inner_value
54+
55+
56+
@final
57+
class _Missing(Immutable):
58+
"""Represents a missing value for reducers."""
59+
60+
__slots__ = ('_instance',)
61+
62+
_instance: Optional['_Missing'] = None
63+
64+
def __new__(cls, *args: Any, **kwargs: Any) -> '_Missing':
65+
if cls._instance is None:
66+
cls._instance = object.__new__(cls) # noqa: WPS609
67+
return cls._instance
68+
69+
70+
#: A singleton representing any missing value
71+
Missing = _Missing()
72+
73+
74+
def transduce(
75+
xform: Callable[
76+
[Callable[[_AccValueType, _ValueType], _AccValueType]],
77+
Callable[[_AccValueType, _ValueType], _AccValueType],
78+
],
79+
reducing_function: Callable[[_AccValueType, _ValueType], _AccValueType],
80+
initial: _AccValueType,
81+
iterable: Iterable[_ValueType],
82+
) -> _AccValueType:
83+
"""
84+
Process information with transducers.
85+
86+
.. code:: python
87+
88+
>>> from returns.transducers import tmap, transduce
89+
90+
>>> def add_one(number: int) -> int:
91+
... return number + 1
92+
93+
>>> def add(acc: int, number: int) -> int:
94+
... return acc + number
95+
96+
>>> my_list = [0, 1, 2]
97+
>>> assert transduce(tmap(add_one), add, 0, my_list) == 6
98+
"""
99+
reducer = xform(reducing_function)
100+
return treduce(reducer, iterable, initial)
101+
102+
103+
@overload
104+
def treduce(
105+
function: Callable[[_ValueType, _ValueType], _ValueType],
106+
iterable: Iterable[_ValueType],
107+
initial: _Missing = Missing,
108+
) -> _ValueType:
109+
"""Reduce without an initial value."""
110+
111+
112+
@overload
113+
def treduce(
114+
function: Callable[[_AccValueType, _ValueType], _AccValueType],
115+
iterable: Iterable[_ValueType],
116+
initial: _AccValueType,
117+
) -> _AccValueType:
118+
"""Reduce with an initial value."""
119+
120+
121+
def treduce(function, iterable, initial=Missing):
122+
"""
123+
A rewritten version of :func:`reduce <functools.reduce>`.
124+
125+
This version considers some features borrowed from Clojure:
126+
127+
- Early termination
128+
- Function initializer [TODO]
129+
130+
You can use it as a normal reduce if you want:
131+
132+
.. code:: python
133+
134+
>>> from returns.transducers import treduce
135+
136+
>>> def add(acc: int, value: int) -> int:
137+
... return acc + value
138+
139+
>>> assert treduce(add, [1, 2, 3]) == 6
140+
141+
"""
142+
it = iter(iterable)
143+
144+
if initial is Missing:
145+
try:
146+
acc_value = next(it)
147+
except StopIteration:
148+
raise TypeError(
149+
'reduce() of empty iterable with no initial value',
150+
) from None
151+
else:
152+
acc_value = initial
153+
154+
for value in it: # noqa: WPS110
155+
acc_value = function(acc_value, value)
156+
if isinstance(acc_value, Reduced):
157+
return acc_value.value
158+
return acc_value

setup.cfg

+3
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ per-file-ignores =
6969
returns/methods/__init__.py: F401, WPS201
7070
returns/pipeline.py: F401
7171
returns/context/__init__.py: F401, WPS201
72+
returns/transducers/__init__.py: F401
7273
# Disable some quality checks for the most heavy parts:
7374
returns/io.py: WPS402
7475
returns/iterables.py: WPS234
@@ -77,6 +78,8 @@ per-file-ignores =
7778
returns/primitives/asserts.py: S101
7879
# Some rules cannot be applied to context:
7980
returns/context/*.py: WPS201, WPS204, WPS226, WPS326, WPS430
81+
# Some rules cannot be applied to transducers:
82+
returns/transducers/*.py: WPS110, WPS430
8083
# We allow `futures` to do attribute access:
8184
returns/future.py: WPS437
8285
returns/_internal/futures/*.py: WPS204, WPS433, WPS437
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
from returns.transducers.transducers import _Missing
2+
3+
4+
def test_missing_singleton():
5+
"""Ensures `_Missing` is a singleton."""
6+
assert _Missing() is _Missing()
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import pytest
2+
3+
from returns.transducers import treduce
4+
5+
6+
def test_reduce():
7+
"""Should fail when iterable is empty and non initial value is given."""
8+
with pytest.raises(TypeError):
9+
treduce(lambda acc, value: acc + value, []) # noqa: WPS110
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
- case: tfilter
2+
disable_cache: false
3+
main: |
4+
from returns.transducers import tfilter
5+
6+
def is_even(number: int) -> bool:
7+
...
8+
9+
reveal_type(tfilter(is_even)) # N: Revealed type is 'def [_AccType] (def (_AccType`-2, builtins.int*) -> _AccType`-2) -> def (_AccType`-2, builtins.int*) -> _AccType`-2'
10+
11+
12+
- case: tfilter_reducer
13+
disable_cache: false
14+
main: |
15+
from typing import List
16+
from returns.transducers import tfilter
17+
18+
def is_even(number: int) -> bool:
19+
...
20+
21+
def append(collection: List[int], item: int) -> List[int]:
22+
...
23+
24+
reveal_type(tfilter(is_even)(append)) # N: Revealed type is 'def (builtins.list*[builtins.int], builtins.int) -> builtins.list*[builtins.int]'
25+
26+
27+
- case: tfilter_reducer_filter_
28+
disable_cache: false
29+
main: |
30+
from typing import List
31+
from returns.transducers import tfilter, reduce
32+
33+
def is_even(number: int) -> bool:
34+
...
35+
36+
def append(collection: List[int], item: int) -> List[int]:
37+
...
38+
39+
my_list: List[int]
40+
reveal_type(tfilter(is_even)(append)(my_list, 2)) # N: Revealed type is 'builtins.list*[builtins.int]'
41+
42+
43+
- case: tfilter_composition_one
44+
disable_cache: false
45+
main: |
46+
from typing import List
47+
from returns.transducers import tfilter, reduce
48+
49+
def is_even(number: int) -> bool:
50+
...
51+
52+
def append(collection: List[int], item: int) -> List[int]:
53+
...
54+
55+
composed = tfilter(is_even)(tfilter(is_even)(append))
56+
reveal_type(composed) # N: Revealed type is 'def (builtins.list*[builtins.int], builtins.int) -> builtins.list*[builtins.int]'
57+
58+
59+
- case: tfilter_composition_two
60+
disable_cache: false
61+
main: |
62+
from typing import List
63+
from returns.transducers import tfilter, reduce
64+
65+
def is_even(number: int) -> bool:
66+
...
67+
68+
def append(collection: List[int], item: int) -> List[int]:
69+
...
70+
71+
composed = tfilter(is_even)(tfilter(is_even)(append))
72+
my_list: List[int]
73+
reveal_type(composed(my_list, 42)) # N: Revealed type is 'builtins.list*[builtins.int]'

0 commit comments

Comments
 (0)