Skip to content
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

Issue 1114 #1131

Open
wants to merge 10 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
56 changes: 56 additions & 0 deletions returns/interfaces/filterable.py
Original file line number Diff line number Diff line change
@@ -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(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Future note: we can also make an overload for TypeGuard case.
See python/typeshed#6140

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you elaborate a bit what can we do with type guard here? We are not filtering for example a list with different types. Filter executed on _FilterableType always returns _FilterableType. Did I miss something?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nevermind, this was a self note 🙂
This is even not supported by mypy yet.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It looks like mypy supports TypeGuard now 🎉 :

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]
37 changes: 36 additions & 1 deletion returns/maybe.py
Original file line number Diff line number Diff line change
Expand Up @@ -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]],
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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)
Expand All @@ -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))
Expand Down Expand Up @@ -448,7 +481,9 @@ def maybe(
Requires our :ref:`mypy plugin <mypy-plugins>`.

"""

@wraps(function)
def decorator(*args, **kwargs):
return Maybe.from_optional(function(*args, **kwargs))

return decorator
1 change: 1 addition & 0 deletions returns/pointfree/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
41 changes: 41 additions & 0 deletions returns/pointfree/filter.py
Original file line number Diff line number Diff line change
@@ -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
11 changes: 11 additions & 0 deletions tests/test_maybe/test_maybe_filter.py
Original file line number Diff line number Diff line change
@@ -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