From 6dd1d3637e6f50a60f41fffe79d5e81b39bd2b2c Mon Sep 17 00:00:00 2001 From: Samuele Reghenzi Date: Thu, 31 Dec 2020 13:41:00 +0100 Subject: [PATCH 1/3] add some immutability safety --- monads/list.py | 18 +++++++++--------- monads/maybe.py | 12 ++++++------ monads/monoid.py | 18 +++++++++++------- monads/set.py | 18 +++++++++--------- tests/test_monoids.py | 8 ++++++++ 5 files changed, 43 insertions(+), 31 deletions(-) diff --git a/monads/list.py b/monads/list.py index 070e7ff..2a21519 100644 --- a/monads/list.py +++ b/monads/list.py @@ -27,15 +27,15 @@ class List(Monad[T], Monoidal[list]): return List([value]) def bind(self, function: Callable[[T], List[S]]) -> List[S]: - return reduce(List.mappend, map(function, self.value), List.mzero()) + return reduce(List.mappend, map(function, self._value), List.mzero()) def map(self, function: Callable[[T], S]) -> List[S]: - return List(list(map(function, self.value))) + return List(list(map(function, self._value))) def apply(self, functor: List[Callable[[T], S]]) -> List[S]: return List( - list(chain.from_iterable([map(f, self.value) for f in functor.value])) + list(chain.from_iterable([map(f, self._value) for f in functor._value])) ) @classmethod @@ -64,7 +64,7 @@ class List(Monad[T], Monoidal[list]): return List(reduce(flat, self, List.mzero())) # type: ignore def sort(self, key: Optional[str] = None, reverse: bool = False) -> List[T]: - lst_copy = self.value.copy() + lst_copy = self._value.copy() lst_copy.sort(key=key, reverse=reverse) # type: ignore return List(lst_copy) @@ -75,22 +75,22 @@ class List(Monad[T], Monoidal[list]): functor = uncurry(cast(CurriedBinary, func)) else: functor = func - return reduce(functor, self.value, base_val) # type: ignore + return reduce(functor, self._value, base_val) # type: ignore __and__ = lambda other, self: List.apply(self, other) # type: ignore def mappend(self, other: List[T]) -> List[T]: - return List(self.value + other.value) + return List(self._value + other._value) __add__ = mappend __mul__ = __rmul__ = map __rshift__ = bind def __sizeof__(self) -> int: - return self.value.__sizeof__() + return self._value.__sizeof__() def __len__(self) -> int: - return len(list(self.value)) + return len(list(self._value)) def __iter__(self) -> Iterator[T]: - return iter(self.value) + return iter(self._value) diff --git a/monads/maybe.py b/monads/maybe.py index 9ac2e6b..c320a97 100644 --- a/monads/maybe.py +++ b/monads/maybe.py @@ -176,19 +176,19 @@ class First(Monoid[Maybe[T]]): return First(Nothing()) def mappend(self, other: First): - if isinstance(self.value, Just): + if isinstance(self._value, Just): return self else: return other def __repr__(self) -> str: # pragma: no cover - return f"" + return f"" __add__ = mappend def first(xs: List[Maybe[T]]) -> Maybe[T]: - return First.mconcat(map(lambda x: First(x), xs)).value + return First.mconcat(map(lambda x: First(x), xs))._value class Last(Monoid[Maybe[T]]): @@ -197,16 +197,16 @@ class Last(Monoid[Maybe[T]]): return Last(Nothing()) def mappend(self, other: Last): - if isinstance(other.value, Just): + if isinstance(other._value, Just): return other else: return self def __repr__(self) -> str: # pragma: no cover - return f"" + return f"" __add__ = mappend def last(xs: List[Maybe[T]]) -> Maybe[T]: - return Last.mconcat(map(lambda x: Last(x), xs)).value + return Last.mconcat(map(lambda x: Last(x), xs))._value diff --git a/monads/monoid.py b/monads/monoid.py index be85eeb..716697c 100644 --- a/monads/monoid.py +++ b/monads/monoid.py @@ -2,14 +2,14 @@ from __future__ import annotations from functools import reduce from numbers import Complex from decimal import Decimal -from typing import Any, Callable, Generic, Iterator, Type, TypeVar, Union +from typing import Any, Callable, Generic, Iterator, Type, TypeVar, Union, Final T = TypeVar("T") class Monoid(Generic[T]): def __init__(self, value: T) -> None: - self.value = value + self._value: Final[T] = value # FIXME: Other type set to Any, as the proper value (Monoid[T]) is # reported as incompatible with subclass implementations due to a @@ -29,15 +29,19 @@ class Monoid(Generic[T]): return ( isinstance(other, Monoid) and type(self) == type(other) - and self.value == other.value + and self._value == other._value ) __add__ = mappend + @property + def value(self) -> T: + return self._value + class Monoidal(Monoid[T]): def __repr__(self): # pragma: no cover - return repr(self.value) + return repr(self._value) class String(Monoidal[str]): @@ -46,7 +50,7 @@ class String(Monoidal[str]): return cls(str()) def mappend(self, other: String) -> String: - return String(self.value + other.value) + return String(self._value + other._value) __add__ = mappend @@ -57,7 +61,7 @@ class Addition(Monoidal[Union[int, float]]): return cls(0) def mappend(self, other: Addition) -> Addition: - return Addition(self.value + other.value) + return Addition(self._value + other._value) __add__ = mappend @@ -68,6 +72,6 @@ class Multiplication(Monoidal[Union[int, float]]): return cls(1) def mappend(self, other: Multiplication) -> Multiplication: - return Multiplication(self.value * other.value) + return Multiplication(self._value * other._value) __add__ = mappend diff --git a/monads/set.py b/monads/set.py index 4bb4454..498b42b 100644 --- a/monads/set.py +++ b/monads/set.py @@ -38,15 +38,15 @@ class Set(Monad[T], Monoidal[set]): return Set(unpack(value)) def bind(self, function: Callable[[T], Set[S]]) -> Set[S]: - return reduce(Set.mappend, map(function, self.value), Set.mzero()) + return reduce(Set.mappend, map(function, self._value), Set.mzero()) def map(self, function: Callable[[T], S]) -> Set[S]: - return Set(set(map(function, self.value))) + return Set(set(map(function, self._value))) def apply(self, functor: Set[Callable[[T], S]]) -> Set[S]: return Set( - set(chain.from_iterable([map(f, self.value) for f in functor.value])) + set(chain.from_iterable([map(f, self._value) for f in functor._value])) ) @classmethod @@ -75,7 +75,7 @@ class Set(Monad[T], Monoidal[set]): return Set(reduce(flat, self, Set.mzero())) # type: ignore def sort(self, key: Optional[str] = None, reverse: bool = False) -> Set[T]: - lst_copy = self.value.copy() + lst_copy = self._value.copy() lst_copy.sort(key=key, reverse=reverse) # type: ignore return Set(lst_copy) @@ -86,22 +86,22 @@ class Set(Monad[T], Monoidal[set]): functor = uncurry(cast(CurriedBinary, func)) else: functor = func - return reduce(functor, self.value, base_val) # type: ignore + return reduce(functor, self._value, base_val) # type: ignore __and__ = lambda other, self: Set.apply(self, other) # type: ignore def mappend(self, other: Set[T]) -> Set[T]: - return Set(self.value.union(other.value)) + return Set(self._value.union(other._value)) __add__ = mappend __mul__ = __rmul__ = map __rshift__ = bind def __sizeof__(self) -> int: - return self.value.__sizeof__() + return self._value.__sizeof__() def __len__(self) -> int: - return len(set(self.value)) + return len(set(self._value)) def __iter__(self) -> Iterator[T]: - return iter(self.value) + return iter(self._value) diff --git a/tests/test_monoids.py b/tests/test_monoids.py index 927a11c..78161dc 100644 --- a/tests/test_monoids.py +++ b/tests/test_monoids.py @@ -55,3 +55,11 @@ def test_mconcat(constructor: Constructor) -> None: c: Monoid = construct(constructor, 3) expected: Monoid = a.mappend(b).mappend(c) assert expected == cls.mconcat([a, b, c]) + + +def test_immutability(constructor: Constructor) -> None: + a: Monoid = construct(constructor, 1) + with pytest.raises(AttributeError) as excinfo: + # this is ignore on porpouse othewise the mypy test fail. Uncomment to check the Final check with mypy + a.value = 2 # type: ignore + assert "can't set attribute" in str(excinfo.value) From 9869a5cd4996c87bef84ebea7a2c58df30c6871e Mon Sep 17 00:00:00 2001 From: Samuele Reghenzi Date: Thu, 31 Dec 2020 13:50:03 +0100 Subject: [PATCH 2/3] improved coverage --- tests/test_monoids.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/test_monoids.py b/tests/test_monoids.py index 78161dc..d444b5e 100644 --- a/tests/test_monoids.py +++ b/tests/test_monoids.py @@ -59,6 +59,7 @@ def test_mconcat(constructor: Constructor) -> None: def test_immutability(constructor: Constructor) -> None: a: Monoid = construct(constructor, 1) + assert a.value == 1 with pytest.raises(AttributeError) as excinfo: # this is ignore on porpouse othewise the mypy test fail. Uncomment to check the Final check with mypy a.value = 2 # type: ignore From 52819c35076b7c149c92e3540ae805b0beb953eb Mon Sep 17 00:00:00 2001 From: Samuele Reghenzi Date: Thu, 31 Dec 2020 16:26:09 +0100 Subject: [PATCH 3/3] Docs and tests --- README.md | 3 +++ tests/test_monoids.py | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index a2ae132..af15c83 100644 --- a/README.md +++ b/README.md @@ -147,6 +147,9 @@ value if the list is empty. ## Monads +Wrapped values should be immutable: they are _protected_ from accidental direct writing with *Final* type and the pythonic naming convention. + + ### Maybe[T] Represents optional data. A `Maybe` instance of a certain type `T` will diff --git a/tests/test_monoids.py b/tests/test_monoids.py index d444b5e..c5e19bb 100644 --- a/tests/test_monoids.py +++ b/tests/test_monoids.py @@ -59,7 +59,7 @@ def test_mconcat(constructor: Constructor) -> None: def test_immutability(constructor: Constructor) -> None: a: Monoid = construct(constructor, 1) - assert a.value == 1 + assert a.value != None with pytest.raises(AttributeError) as excinfo: # this is ignore on porpouse othewise the mypy test fail. Uncomment to check the Final check with mypy a.value = 2 # type: ignore