diff --git a/CHANGELOG.md b/CHANGELOG.md index 4851b782d..67a06ee8d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,11 @@ Versions before `1.0.0` are `0Ver`-based: incremental in minor, bugfixes only are patches. See [0Ver](https://0ver.org/). +## 0.17.2 + +### Features +- Add `filter` for `Maybe` + ## 0.17.1 ### Bugfixes diff --git a/returns/interfaces/filterable.py b/returns/interfaces/filterable.py new file mode 100644 index 000000000..e445ea4c8 --- /dev/null +++ b/returns/interfaces/filterable.py @@ -0,0 +1,56 @@ +from abc import abstractmethod +from typing import Callable, NoReturn, TypeVar + +from returns.interfaces.specific.maybe import MaybeLikeN +from returns.primitives.hkt import Kind1 + +_FirstType = TypeVar('_FirstType') +_SecondType = TypeVar('_SecondType') +_ThirdType = TypeVar('_ThirdType') + +_FilterableType = TypeVar('_FilterableType', bound='FilterableN') + + +class FilterableN(MaybeLikeN[_FirstType, _SecondType, _ThirdType]): + """ + Represents container that can apply filter over inner value. + + There are no aliases or ``FilterableN` for ``Filterable`` interface. + Because it always uses one type. + + Not all types can be ``Filterable`` because we require + a possibility to access internal value and to model a case, + where the predicate is false + + .. code:: python + + >>> from returns.maybe import Nothing, Some + >>> from returns.pointfree import filter_ + + >>> def is_even(argument: int) -> bool: + ... return argument % 2 == 0 + + >>> assert filter_(is_even)(Some(5)) == Nothing + >>> assert filter_(is_even)(Some(6)) == Some(6) + >>> assert filter_(is_even)(Nothing) == Nothing + + """ + + __slots__ = () + + @abstractmethod + def filter( + self: _FilterableType, + predicate: Callable[[_FirstType], bool], + ) -> Kind1[_FilterableType, _FirstType]: + """Applies 'predicate' to the result of a previous computation.""" + + +#: Type alias for kinds with one type argument. +Filterable1 = FilterableN[_FirstType, NoReturn, NoReturn] + +#: Type alias for kinds with two type arguments. +Filterable2 = FilterableN[_FirstType, _SecondType, NoReturn] + +#: Type alias for kinds with three type arguments. +Filterable3 = FilterableN[_FirstType, _SecondType, _ThirdType] diff --git a/returns/maybe.py b/returns/maybe.py index 7a443059a..c0d591baa 100644 --- a/returns/maybe.py +++ b/returns/maybe.py @@ -141,6 +141,29 @@ def bind_optional( """ + def filter( + self, + function: Callable[[_ValueType], bool], + ) -> 'Maybe[_ValueType]': + """ + Apply a predicate over the value. + + If the predicate returns true, + it returns the original value wrapped with Some. + If the predicate returns false, Nothing is returned + + .. code:: python + + >>> from returns.maybe import Some, Nothing + >>> def predicate(value): + ... return value % 2 == 0 + + >>> assert Some(5).filter(predicate) == Nothing + >>> assert Some(6).filter(predicate) == Some(6) + >>> assert Nothing.filter(predicate) == Nothing + + """ + def lash( self, function: Callable[[Any], Kind1['Maybe', _ValueType]], @@ -338,6 +361,10 @@ def bind_optional(self, function): """Does nothing.""" return self + def filter(self, function): + """Does nothing.""" + return self + def lash(self, function): """Composes this container with a function returning container.""" return function(None) @@ -375,7 +402,7 @@ def __init__(self, inner_value: _ValueType) -> None: """Some constructor.""" super().__init__(inner_value) - if not TYPE_CHECKING: # noqa: WPS604 # pragma: no branch + if not TYPE_CHECKING: # noqa: WPS604,C901 # pragma: no branch def bind(self, function): """Binds current container to a function that returns container.""" return function(self._inner_value) @@ -388,6 +415,12 @@ def unwrap(self): """Returns inner value for successful container.""" return self._inner_value + def filter(self, function): + """Filters internal value.""" + if function(self._inner_value): + return self + return _Nothing() + def map(self, function): """Composes current container with a pure function.""" return Some(function(self._inner_value)) @@ -448,7 +481,9 @@ def maybe( Requires our :ref:`mypy plugin `. """ + @wraps(function) def decorator(*args, **kwargs): return Maybe.from_optional(function(*args, **kwargs)) + return decorator diff --git a/returns/pointfree/__init__.py b/returns/pointfree/__init__.py index bbece84cc..bd0105596 100644 --- a/returns/pointfree/__init__.py +++ b/returns/pointfree/__init__.py @@ -35,6 +35,7 @@ from returns.pointfree.bind_result import bind_result as bind_result from returns.pointfree.compose_result import compose_result as compose_result from returns.pointfree.cond import cond as cond +from returns.pointfree.filter import filter_ as filter_ from returns.pointfree.lash import lash as lash from returns.pointfree.map import map_ as map_ from returns.pointfree.modify_env import modify_env as modify_env diff --git a/returns/pointfree/filter.py b/returns/pointfree/filter.py new file mode 100644 index 000000000..03d37e364 --- /dev/null +++ b/returns/pointfree/filter.py @@ -0,0 +1,41 @@ +from typing import Callable, TypeVar + +from returns.interfaces.filterable import FilterableN +from returns.primitives.hkt import Kinded, KindN, kinded + +_FirstType = TypeVar('_FirstType') +_SecondType = TypeVar('_SecondType') +_ThirdType = TypeVar('_ThirdType') +_FilterableKind = TypeVar('_FilterableKind', bound=FilterableN) + + +def filter_( + predicate: Callable[[_FirstType], bool], +) -> Kinded[Callable[ + [KindN[_FilterableKind, _FirstType, _SecondType, _ThirdType]], + KindN[_FilterableKind, _FirstType, _SecondType, _ThirdType], +]]: + """ + Applies predicate over container. + + This is how it should be used: + + .. code:: python + + >>> from returns.maybe import Some, Nothing + + >>> def example(value): + ... return value % 2 == 0 + + >>> assert filter_(example)(Some(5)) == Nothing + >>> assert filter_(example)(Some(6)) == Some(6) + + """ + + @kinded + def factory( + container: KindN[_FilterableKind, _FirstType, _SecondType, _ThirdType], + ) -> KindN[_FilterableKind, _FirstType, _SecondType, _ThirdType]: + return container.filter(predicate) + + return factory diff --git a/tests/test_maybe/test_maybe_filter.py b/tests/test_maybe/test_maybe_filter.py new file mode 100644 index 000000000..d24bb7fb5 --- /dev/null +++ b/tests/test_maybe/test_maybe_filter.py @@ -0,0 +1,11 @@ +from returns.maybe import Nothing, Some + + +def test_maybe_filter(): + """Ensures that .filter works correctly.""" + def factory(argument: int) -> bool: + return argument % 2 == 0 + + assert Some(5).filter(factory) == Nothing + assert Some(6).filter(factory) == Some(6) + assert Nothing.filter(factory) == Nothing