-
-
Notifications
You must be signed in to change notification settings - Fork 123
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Adds transducers support #904
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
.. transducers: | ||
|
||
Transducers | ||
=========== | ||
|
||
API Reference | ||
------------- | ||
|
||
.. automodule:: returns.transducers.transducers | ||
:members: | ||
|
||
.. autofunction:: returns.transducers.tmap | ||
|
||
.. autofunction:: returns.transducers.tfilter |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
from returns.transducers.tfilter import tfilter as tfilter | ||
from returns.transducers.tmap import tmap as tmap | ||
from returns.transducers.transducers import Missing as Missing | ||
from returns.transducers.transducers import Reduced as Reduced | ||
from returns.transducers.transducers import transduce as transduce | ||
from returns.transducers.transducers import treduce as treduce |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,41 @@ | ||
from typing import Callable, TypeVar | ||
|
||
_ValueType = TypeVar('_ValueType') | ||
_AccValueType = TypeVar('_AccValueType') | ||
|
||
|
||
def tfilter( | ||
predicate: Callable[[_ValueType], bool], | ||
) -> Callable[ | ||
[Callable[[_AccValueType, _ValueType], _AccValueType]], | ||
Callable[[_AccValueType, _ValueType], _AccValueType], | ||
]: | ||
""" | ||
:py:func:`filter <filter>` implementation on a transducer form. | ||
|
||
.. code:: python | ||
|
||
>>> from typing import List | ||
>>> from returns.transducers import tfilter, treduce | ||
|
||
>>> def is_even(number: int) -> bool: | ||
... return number % 2 == 0 | ||
|
||
>>> def append(collection: List[int], item: int) -> List[int]: | ||
... collection.append(item) | ||
... return collection | ||
|
||
>>> my_list = [0, 1, 2, 3, 4, 5, 6] | ||
>>> xform = tfilter(is_even)(append) | ||
>>> assert treduce(xform, my_list, []) == [0, 2, 4, 6] | ||
|
||
""" | ||
def reducer( | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We tend to use There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yep, but I think using that pattern might be weird: def tmap(function):
def reducer(step):
def map_(acc, value):
...
return map_
return reducer
def tmap(function):
def decorator(step):
def factory(acc, value):
...
return map_
return reducer In fact transducers is not intended to be use as a decorator and I think |
||
step: Callable[[_AccValueType, _ValueType], _AccValueType], | ||
) -> Callable[[_AccValueType, _ValueType], _AccValueType]: | ||
def filter_(acc: _AccValueType, value: _ValueType) -> _AccValueType: | ||
if predicate(value): | ||
return step(acc, value) | ||
return acc | ||
return filter_ | ||
return reducer |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,41 @@ | ||
from typing import Callable, TypeVar | ||
|
||
_ValueType = TypeVar('_ValueType') | ||
_NewValueType = TypeVar('_NewValueType') | ||
|
||
_AccValueType = TypeVar('_AccValueType') | ||
|
||
|
||
def tmap( | ||
function: Callable[[_ValueType], _NewValueType], | ||
) -> Callable[ | ||
[Callable[[_AccValueType, _NewValueType], _AccValueType]], | ||
Callable[[_AccValueType, _ValueType], _AccValueType], | ||
]: | ||
""" | ||
A map implementation on a transducer form. | ||
|
||
.. code:: python | ||
|
||
>>> from typing import List | ||
>>> from returns.transducers import tmap, treduce | ||
|
||
>>> def add_one(number: int) -> int: | ||
... return number + 1 | ||
|
||
>>> def append(collection: List[int], item: int) -> List[int]: | ||
... collection.append(item) | ||
... return collection | ||
|
||
>>> my_list = [0, 1] | ||
>>> xformaa = tmap(add_one)(append) | ||
>>> assert treduce(xformaa, my_list, []) == [1, 2] | ||
|
||
""" | ||
def reducer( | ||
step: Callable[[_AccValueType, _NewValueType], _AccValueType], | ||
) -> Callable[[_AccValueType, _ValueType], _AccValueType]: | ||
def map_(acc: _AccValueType, value: _ValueType) -> _AccValueType: | ||
return step(acc, function(value)) | ||
return map_ | ||
return reducer |
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
@@ -0,0 +1,158 @@ | ||||||
from typing import ( | ||||||
Any, | ||||||
Callable, | ||||||
Generic, | ||||||
Iterable, | ||||||
Optional, | ||||||
TypeVar, | ||||||
final, | ||||||
overload, | ||||||
) | ||||||
|
||||||
from returns.primitives.types import Immutable | ||||||
|
||||||
_ValueType = TypeVar('_ValueType') | ||||||
_NewValueType = TypeVar('_NewValueType') | ||||||
|
||||||
_AccValueType = TypeVar('_AccValueType') | ||||||
|
||||||
|
||||||
@final | ||||||
class Reduced(Immutable, Generic[_ValueType]): | ||||||
""" | ||||||
Sentinel for early termination inside transducer. | ||||||
|
||||||
.. code:: python | ||||||
|
||||||
>>> from returns.transducers import tmap, transduce, Reduced | ||||||
|
||||||
>>> def add_one(number: int) -> int: | ||||||
... return number + 1 | ||||||
|
||||||
>>> def add(acc: int, number: int) -> int: | ||||||
... if acc == 3: | ||||||
... return Reduced(acc) | ||||||
... return acc + number | ||||||
|
||||||
>>> my_list = [0, 1, 2] | ||||||
>>> assert transduce(tmap(add_one), add, 0, my_list) == 3 | ||||||
|
||||||
""" | ||||||
|
||||||
__slots__ = ('_inner_value',) | ||||||
|
||||||
_inner_value: _ValueType | ||||||
|
||||||
def __init__(self, inner_value: _ValueType) -> None: | ||||||
"""Encapsulates the value from early reduce termination.""" | ||||||
object.__setattr__(self, '_inner_value', inner_value) # noqa: WPS609 | ||||||
|
||||||
@property | ||||||
def value(self) -> _ValueType: # noqa: WPS110 | ||||||
"""Returns the value from early reduce termination.""" | ||||||
return self._inner_value | ||||||
|
||||||
|
||||||
@final | ||||||
class _Missing(Immutable): | ||||||
"""Represents a missing value for reducers.""" | ||||||
|
||||||
__slots__ = ('_instance',) | ||||||
|
||||||
_instance: Optional['_Missing'] = None | ||||||
|
||||||
def __new__(cls, *args: Any, **kwargs: Any) -> '_Missing': | ||||||
if cls._instance is None: | ||||||
cls._instance = object.__new__(cls) # noqa: WPS609 | ||||||
return cls._instance | ||||||
|
||||||
|
||||||
#: A singleton representing any missing value | ||||||
Missing = _Missing() | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
|
||||||
|
||||||
def transduce( | ||||||
xform: Callable[ | ||||||
[Callable[[_AccValueType, _ValueType], _AccValueType]], | ||||||
Callable[[_AccValueType, _ValueType], _AccValueType], | ||||||
], | ||||||
reducing_function: Callable[[_AccValueType, _ValueType], _AccValueType], | ||||||
initial: _AccValueType, | ||||||
iterable: Iterable[_ValueType], | ||||||
) -> _AccValueType: | ||||||
""" | ||||||
Process information with transducers. | ||||||
|
||||||
.. code:: python | ||||||
|
||||||
>>> from returns.transducers import tmap, transduce | ||||||
|
||||||
>>> def add_one(number: int) -> int: | ||||||
... return number + 1 | ||||||
|
||||||
>>> def add(acc: int, number: int) -> int: | ||||||
... return acc + number | ||||||
|
||||||
>>> my_list = [0, 1, 2] | ||||||
>>> assert transduce(tmap(add_one), add, 0, my_list) == 6 | ||||||
""" | ||||||
reducer = xform(reducing_function) | ||||||
return treduce(reducer, iterable, initial) | ||||||
|
||||||
|
||||||
@overload | ||||||
def treduce( | ||||||
function: Callable[[_ValueType, _ValueType], _ValueType], | ||||||
iterable: Iterable[_ValueType], | ||||||
initial: _Missing = Missing, | ||||||
) -> _ValueType: | ||||||
"""Reduce without an initial value.""" | ||||||
|
||||||
|
||||||
@overload | ||||||
def treduce( | ||||||
function: Callable[[_AccValueType, _ValueType], _AccValueType], | ||||||
iterable: Iterable[_ValueType], | ||||||
initial: _AccValueType, | ||||||
) -> _AccValueType: | ||||||
"""Reduce with an initial value.""" | ||||||
|
||||||
|
||||||
def treduce(function, iterable, initial=Missing): | ||||||
""" | ||||||
A rewritten version of :func:`reduce <functools.reduce>`. | ||||||
|
||||||
This version considers some features borrowed from Clojure: | ||||||
|
||||||
- Early termination | ||||||
- Function initializer [TODO] | ||||||
|
||||||
You can use it as a normal reduce if you want: | ||||||
|
||||||
.. code:: python | ||||||
|
||||||
>>> from returns.transducers import treduce | ||||||
|
||||||
>>> def add(acc: int, value: int) -> int: | ||||||
... return acc + value | ||||||
|
||||||
>>> assert treduce(add, [1, 2, 3]) == 6 | ||||||
|
||||||
""" | ||||||
it = iter(iterable) | ||||||
|
||||||
if initial is Missing: | ||||||
try: | ||||||
acc_value = next(it) | ||||||
except StopIteration: | ||||||
raise TypeError( | ||||||
'reduce() of empty iterable with no initial value', | ||||||
) from None | ||||||
else: | ||||||
acc_value = initial | ||||||
|
||||||
for value in it: # noqa: WPS110 | ||||||
acc_value = function(acc_value, value) | ||||||
if isinstance(acc_value, Reduced): | ||||||
return acc_value.value | ||||||
return acc_value |
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
@@ -0,0 +1,6 @@ | ||||||
from returns.transducers.transducers import _Missing | ||||||
|
||||||
|
||||||
def test_missing_singleton(): | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
And all other tests as well. |
||||||
"""Ensures `_Missing` is a singleton.""" | ||||||
assert _Missing() is _Missing() |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
import pytest | ||
|
||
from returns.transducers import treduce | ||
|
||
|
||
def test_reduce(): | ||
"""Should fail when iterable is empty and non initial value is given.""" | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should we? Raising exceptions is not something we do in this library (at least we try to). Can we just return the empty sequence? Or do anything else? |
||
with pytest.raises(TypeError): | ||
treduce(lambda acc, value: acc + value, []) # noqa: WPS110 |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,73 @@ | ||
- case: tfilter | ||
disable_cache: false | ||
main: | | ||
from returns.transducers import tfilter | ||
|
||
def is_even(number: int) -> bool: | ||
... | ||
|
||
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' | ||
|
||
|
||
- case: tfilter_reducer | ||
disable_cache: false | ||
main: | | ||
from typing import List | ||
from returns.transducers import tfilter | ||
|
||
def is_even(number: int) -> bool: | ||
... | ||
|
||
def append(collection: List[int], item: int) -> List[int]: | ||
... | ||
|
||
reveal_type(tfilter(is_even)(append)) # N: Revealed type is 'def (builtins.list*[builtins.int], builtins.int) -> builtins.list*[builtins.int]' | ||
|
||
|
||
- case: tfilter_reducer_filter_ | ||
disable_cache: false | ||
main: | | ||
from typing import List | ||
from returns.transducers import tfilter, reduce | ||
|
||
def is_even(number: int) -> bool: | ||
... | ||
|
||
def append(collection: List[int], item: int) -> List[int]: | ||
... | ||
|
||
my_list: List[int] | ||
reveal_type(tfilter(is_even)(append)(my_list, 2)) # N: Revealed type is 'builtins.list*[builtins.int]' | ||
|
||
|
||
- case: tfilter_composition_one | ||
disable_cache: false | ||
main: | | ||
from typing import List | ||
from returns.transducers import tfilter, reduce | ||
|
||
def is_even(number: int) -> bool: | ||
... | ||
|
||
def append(collection: List[int], item: int) -> List[int]: | ||
... | ||
|
||
composed = tfilter(is_even)(tfilter(is_even)(append)) | ||
reveal_type(composed) # N: Revealed type is 'def (builtins.list*[builtins.int], builtins.int) -> builtins.list*[builtins.int]' | ||
|
||
|
||
- case: tfilter_composition_two | ||
disable_cache: false | ||
main: | | ||
from typing import List | ||
from returns.transducers import tfilter, reduce | ||
|
||
def is_even(number: int) -> bool: | ||
... | ||
|
||
def append(collection: List[int], item: int) -> List[int]: | ||
... | ||
|
||
composed = tfilter(is_even)(tfilter(is_even)(append)) | ||
my_list: List[int] | ||
reveal_type(composed(my_list, 42)) # N: Revealed type is 'builtins.list*[builtins.int]' |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let's make the first line more user-friendly by removing
:py:func:
stuff.It can be later in the body.