-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* Add Maybe base impl (#4) * `Maybe` container * `Some` container * `_Nothing` singletone for empty value * `Nothing` instance * `bind` function * Add tests for `Maybe` monad (#4) * `test_nothing_is_singleton` * `test_bind` (including >> syntax) * Upd commit template (#4) * Add dedicated __rshift__ (#4) * Add `maybe.recover` with tests (#4) * Add `maybe.choose` (#4) * Replace use of Callable with MaybeFunc (#4)
- Loading branch information
Showing
3 changed files
with
210 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -8,6 +8,6 @@ | |
"tests" | ||
], | ||
"vscodeGitCommit.template": [ | ||
"{message}" | ||
"{message} (#{issue})" | ||
] | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,137 @@ | ||
import abc | ||
import dataclasses | ||
import typing | ||
|
||
import toolz | ||
|
||
from mona import state | ||
|
||
T = typing.TypeVar("T") | ||
V = typing.TypeVar("V") | ||
|
||
|
||
@dataclasses.dataclass(frozen=True) | ||
class Maybe(state.State[T], abc.ABC): | ||
"""General purpose container for optional value. | ||
`Maybe` container has 2 variations: `Some` and `Nothing`. `Some` stands for some | ||
actual value and means that it is present in execution context. `Nothing` on the | ||
other hand describes emtpiness and lack of value. This is additional wrapping around | ||
`None` values. | ||
Strict types help ignore | ||
""" | ||
|
||
def __rshift__(self, func: typing.Callable[[T], "Maybe[V]"]) -> "Maybe[V]": | ||
"""Dunder method for `>>` bind syntax. | ||
>>> maybe.bind(function, cnt) | ||
>>> # exactly the same as | ||
>>> cnt >> function | ||
Args: | ||
func (typing.Callable[[T], "Maybe[V]"]): _description_ | ||
Returns: | ||
Maybe[V]: _description_ | ||
""" | ||
return bind(func, self) | ||
|
||
|
||
@dataclasses.dataclass(frozen=True) | ||
class Some(Maybe[T]): | ||
"""Container for actually present value.""" | ||
|
||
|
||
@dataclasses.dataclass(frozen=True) | ||
class Nothing(Maybe[typing.Any]): | ||
"""Private container for non-existent value.""" | ||
|
||
__slots__ = () | ||
__instance: "Nothing | None" = None | ||
|
||
def __new__(cls, *args, **kwargs) -> "Nothing": # noqa | ||
match cls.__instance: | ||
case None: | ||
cls.__instance = object.__new__(cls) | ||
return cls.__instance | ||
case _: | ||
return cls.__instance | ||
|
||
def __init__(self) -> None: # noqa | ||
super().__init__(None) | ||
|
||
|
||
MaybeFunc = typing.Callable[[T], Maybe[V]] | ||
|
||
|
||
@toolz.curry | ||
def bind(func: MaybeFunc[T, V], cnt: Maybe[T]) -> Maybe[V]: | ||
"""Bind function for `Maybe` monad. | ||
`function` is executed only in case `cnt` is `Some` and not `Nothing` as it does not | ||
make any sense to execute some `function` with `Nothing`. | ||
Args: | ||
function (typing.Callable[[Maybe[T]], Maybe[V]]): to bind | ||
cnt (Maybe[T]): `Maybe` container | ||
Returns: | ||
Maybe[V]: result of running `function` on `cnt` value. | ||
""" | ||
match cnt: | ||
case Some(value): | ||
return func(value) | ||
case _: | ||
return cnt | ||
|
||
|
||
def recover(value: T) -> MaybeFunc[V, T | V]: | ||
"""Recovers from `Nothing` or just passes `Some`. | ||
When `Some` value is passed nothing is done and it is just returned. When `Nothing` | ||
is passed than it is repaced with `Some` `value`. | ||
Args: | ||
value (T): to recover to | ||
Returns: | ||
typing.Callable[[Maybe[V]], Maybe[V] | Maybe[T]]: maybe handler function | ||
""" | ||
|
||
def _recover(cnt: Maybe[V]) -> Maybe[V] | Maybe[T]: | ||
match cnt: | ||
case Some(): | ||
return cnt | ||
case _: | ||
return Some(value) | ||
|
||
return _recover | ||
|
||
|
||
def _continue_on_some(cur: MaybeFunc[T, V], nxt: MaybeFunc[T, V]) -> MaybeFunc[T, V]: | ||
def __continue_on_some(val: T): | ||
match cur(val): | ||
case Some(result): | ||
return result | ||
case Nothing(): | ||
return nxt(val) | ||
|
||
return __continue_on_some | ||
|
||
|
||
def choose(*functions: MaybeFunc[T, V]) -> MaybeFunc[T, V]: | ||
"""Return first `Some` result from passed functions. | ||
If `functions` is empty, than return `Nothing`. | ||
If no `function` can return `Some` than return `Nothing`. | ||
""" | ||
|
||
def _choose(value: T): | ||
match functions: | ||
case (): | ||
return Nothing() | ||
case _: | ||
return toolz.reduce(_continue_on_some, functions)(value) | ||
|
||
return _choose |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,72 @@ | ||
import pytest | ||
|
||
from mona import maybe | ||
|
||
|
||
def test_nothing_is_singleton(): | ||
assert maybe.Nothing() == maybe.Nothing() | ||
assert id(maybe.Nothing()) == id(maybe.Nothing()) | ||
|
||
|
||
@pytest.mark.parametrize( | ||
"arrange_function,arrange_cnt,assert_cnt", | ||
[ | ||
(lambda x: maybe.Some(x), maybe.Some(1), maybe.Some(1)), | ||
(lambda x: maybe.Some(x), maybe.Some(2), maybe.Some(2)), | ||
(lambda x: maybe.Some(x), maybe.Nothing(), maybe.Nothing()), | ||
(lambda x: maybe.Nothing(), maybe.Nothing(), maybe.Nothing()), | ||
(lambda x: maybe.Nothing(), maybe.Some(1), maybe.Nothing()), | ||
], | ||
) | ||
def test_bind(arrange_function, arrange_cnt, assert_cnt): | ||
# act | ||
act_cnt = maybe.bind(arrange_function, arrange_cnt) | ||
act_cnt_rshift = arrange_cnt >> arrange_function | ||
|
||
# assert | ||
assert act_cnt == assert_cnt | ||
assert act_cnt_rshift == assert_cnt | ||
|
||
|
||
@pytest.mark.parametrize( | ||
"arrange_recovery_value,arrange_cnt,assert_cnt", | ||
[ | ||
(1, maybe.Some(1), maybe.Some(1)), | ||
(2, maybe.Some(1), maybe.Some(1)), | ||
(2, maybe.Nothing(), maybe.Some(2)), | ||
], | ||
) | ||
def test_recover(arrange_recovery_value, arrange_cnt, assert_cnt): | ||
# arrange | ||
arrange_recover = maybe.recover(arrange_recovery_value) | ||
|
||
# act | ||
act_cnt = arrange_recover(arrange_cnt) | ||
|
||
# assert | ||
assert act_cnt == assert_cnt | ||
|
||
|
||
@pytest.mark.parametrize( | ||
"arrange_functions, arrange_cnt, assert_cnt", | ||
[ | ||
( | ||
[lambda _: maybe.Nothing(), lambda x: maybe.Some(x)], | ||
maybe.Some(1), | ||
maybe.Some(1), | ||
), | ||
( | ||
[lambda _: maybe.Nothing, lambda x: maybe.Some(x)], | ||
maybe.Nothing(), | ||
maybe.Nothing(), | ||
), | ||
([lambda _: maybe.Nothing()], maybe.Nothing(), maybe.Nothing()), | ||
([lambda _: maybe.Nothing()], maybe.Some(1), maybe.Nothing()), | ||
], | ||
) | ||
def test_choose(arrange_functions, arrange_cnt, assert_cnt): | ||
arrange_choose = maybe.choose(*arrange_functions) | ||
|
||
act_cnt = arrange_cnt >> arrange_choose | ||
|
||
assert act_cnt == assert_cnt |