This commit is contained in:
Correl Roush 2018-10-11 20:44:57 -04:00
parent d38f617ba4
commit f5b0e6e5e4
4 changed files with 125 additions and 2 deletions

View file

@ -1,6 +1,7 @@
from __future__ import annotations
from typing import Any, Callable, Generic, Optional, TypeVar
from typing import Any, Callable, Generic, List, Optional, TypeVar
from .monad import Monad
from .monoid import Monoid
T = TypeVar("T")
S = TypeVar("S")
@ -67,3 +68,45 @@ def maybe(value: T, predicate: Optional[Callable[[T], bool]] = None) -> Maybe[T]
return Just(value)
else:
return Nothing()
class First(Monoid[Maybe[T]]):
@classmethod
def mzero(cls) -> First:
return First(Nothing())
def mappend(self, other: First):
if isinstance(self.value, Just):
return self
else:
return other
def __repr__(self) -> str:
return f"<First {self.value}>"
__add__ = mappend
def first(xs: List[Maybe[T]]) -> Maybe[T]:
return First.mconcat(map(lambda x: First(x), xs)).value
class Last(Monoid[Maybe[T]]):
@classmethod
def mzero(cls) -> Last:
return Last(Nothing())
def mappend(self, other: Last):
if isinstance(other.value, Just):
return other
else:
return self
def __repr__(self) -> str:
return f"<Last {self.value}>"
__add__ = mappend
def last(xs: List[Maybe[T]]) -> Maybe[T]:
return Last.mconcat(map(lambda x: Last(x), xs)).value

34
monads/monoid.py Normal file
View file

@ -0,0 +1,34 @@
from __future__ import annotations
from functools import reduce
from numbers import Number
from typing import Any, Callable, Generic, Iterator, List, Type, TypeVar, Union
T = TypeVar("T")
class Monoid(Generic[T]):
def __init__(self, value: T) -> None:
self.value = value
# FIXME: Other type set to Any, as the proper value (Monoid[T]) is
# reported as incompatible with subclass implementations due to a
# flaw in mypy: https://github.com/python/mypy/issues/1317
def mappend(self, other: Any) -> Monoid[T]:
raise NotImplementedError
@classmethod
def mzero(cls) -> Monoid[T]:
raise NotImplementedError
@classmethod
def mconcat(cls, xs: Iterator[Monoid[T]]) -> Monoid[T]:
return reduce(cls.mappend, xs, cls.mzero())
def __eq__(self, other: object) -> bool:
return (
isinstance(other, Monoid)
and type(self) == type(other)
and self.value == other.value
)
__add__ = mappend

View file

@ -1,4 +1,6 @@
from monads.maybe import Maybe, Just, Nothing, maybe
import pytest # type: ignore
from typing import List
from monads.maybe import Maybe, Just, Nothing, maybe, first, last
def test_maybe_none():
@ -15,3 +17,12 @@ def test_maybe_boolean_false():
def test_maybe_boolean_true():
assert isinstance(maybe(True, predicate=bool), Just)
def test_first() -> None:
maybes: List[Maybe[int]] = [Nothing(), Just(1), Just(2)]
assert Just(1) == first(maybes)
def test_last() -> None:
maybes: List[Maybe[int]] = [Just(1), Just(2), Nothing()]
assert Just(2) == last(maybes)

35
tests/test_monoids.py Normal file
View file

@ -0,0 +1,35 @@
import pytest # type: ignore
from typing import Any, Callable, Type
from monads.monoid import Monoid
from monads.maybe import First, Last, Just
Constructor = Callable[[Any], Monoid]
@pytest.fixture(
scope="module", params=[lambda x: First(Just(x)), lambda x: Last(Just(x))]
)
def monoid(request) -> Constructor:
return request.param
def test_associative(monoid: Constructor) -> None:
a: Monoid = monoid(1)
b: Monoid = monoid(2)
c: Monoid = monoid(3)
assert (a + b) + c == a + (b + c)
def test_mconcat_empty(monoid: Constructor) -> None:
cls: Type = type(monoid(1))
zero: Monoid = cls.mzero()
assert zero == cls.mconcat([])
def test_mconcat(monoid: Constructor) -> None:
cls: Type = type(monoid(1))
a: Monoid = monoid(1)
b: Monoid = monoid(2)
c: Monoid = monoid(3)
expected: Monoid = a.mappend(b).mappend(c)
assert expected == cls.mconcat([a, b, c])