diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml new file mode 100644 index 0000000..0d97a02 --- /dev/null +++ b/.github/workflows/build.yaml @@ -0,0 +1,76 @@ +on: + push: + branches: + - master + pull_request: + branches: + - master + +name: Build + +jobs: + checking: + name: Lint & Test + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v2 + + - name: Setup Python + uses: actions/setup-python@v2 + with: + python-version: "3.x" + - name: Install Dev dependencies + run: | + echo ${{github.ref}} + pip install pytest black mypy pytest-asyncio pytest-cov pytest-black pytest-mypy + pip install -e . + + - name: Run Tests + run: python setup.py test + - uses: codecov/codecov-action@v1 + with: + file: ./reports/coverage.xml + flags: unittests + fail_ci_if_error: true + verbose: true + + releasing: + name: Build Release + runs-on: ubuntu-latest + needs: checking + if: github.ref == 'refs/heads/master' + steps: + - name: Checkout + uses: actions/checkout@v2 + - name: Setup Python + uses: actions/setup-python@v2 + with: + python-version: "3.x" + - name: Install Dev dependencies + run: | + pip install black + - name: Release + id: release + uses: rymndhng/release-on-push-action@v0.14.0 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + bump_version_scheme: patch + + - name: Check Output Parameters + run: echo "Got version ${{ steps.release.outputs.tag_name }}" + - name: Print Version + run: | + sed -i '$ d' monads/__init__.py + echo "version='${{ steps.release.outputs.tag_name }}'" >> monads/__init__.py + sed -i '9s/.*/ version="${{ steps.release.outputs.tag_name }}",/' setup.py + black setup.py + black monads/__init__.py + - uses: mikeal/publish-to-github-action@master + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + + + diff --git a/.github/workflows/todo.yaml b/.github/workflows/todo.yaml new file mode 100644 index 0000000..228d1af --- /dev/null +++ b/.github/workflows/todo.yaml @@ -0,0 +1,22 @@ +on: + push: + branches: + - master + +name: "TODO to Issue Check" + +jobs: + build: + runs-on: "ubuntu-latest" + steps: + - uses: "actions/checkout@master" + - name: "TODO to Issue" + uses: "alstr/todo-to-issue-action@v1.2-beta" + with: + REPO: ${{ github.repository }} + BEFORE: ${{ github.event.before }} + SHA: ${{ github.sha }} + TOKEN: ${{ secrets.GITHUB_TOKEN }} + LABEL: "# TODO" + COMMENT_MARKER: "#" + id: "todo" \ No newline at end of file diff --git a/.gitignore b/.gitignore index 6f7a6d9..9d049fb 100644 --- a/.gitignore +++ b/.gitignore @@ -109,3 +109,4 @@ venv.bak/ .mypy_cache/ .dmypy.json dmypy.json +.vscode diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 504f9e7..0000000 --- a/.travis.yml +++ /dev/null @@ -1,21 +0,0 @@ -language: python -python: 3.7 -dist: xenial -sudo: true -before_install: - - pip install codecov - - pip install black -install: - - pip install -e . -script: - - python setup.py test -after_success: - - codecov -deploy: - provider: pypi - user: correl - password: - secure: JwxQbfLuUW0wT6nYSSDBeRxLCkDJcyD5Jh5anfUJncGDM93O9fy+vSY2HhYTS3f8ZI42dQHicD+dG9TLiiU2I0BxiimEu2dRNxJGkkqeC7p97m4/+fPPXmLgaGyxFzqnU4vFNlDYtcd/jFQaQaT1PtaL2FVgJH0lEYoXGOa4T2MBDBqaJjbPtxFIUnEnpQsKDLBu2Pnj99z8zgTI5Ob7Rtt0RDT4lrt+A2bHHtIk3PAD6LKUebVFrQLr/XEOTDjzlkfqh3ZY3VH73OXWWXN41IJEYcEmnxDtWqT71VKD/bKPRsP2B/bIiB6fJSvmqkaoWenpizZglgW//WSQvt3nlAcZGGEHZ+0JsY/fKdTwF4ZPlPaOg+ssMt8OXkIGLk3wyPy0UUH6uMD6QkyPy/+9nRzqJv7UqQuyoqxwSmFpXvPQ07YErLakV+fGzKU4pqmRBQzAY+amj32ZNJuFKWuDMAU2xWilLnhtfdpxcwPDJxTetSCVMPr3nnYN9LWYckT9J14UfnN+OzgeLyoEpBuYe9DyB85wC2BIFizqRkxeDAmD/1cwh0Usjdb8gNzdIc8hUezarUkjEyUbgnnXMy1Zp0jrbMu8UVMIOpHpARVaz2BWTzpXEdef+NpDyQKs96kTooj7ezBnf7LU1Jam0haHhTZE4qkcGytjaL5qYLUYHRo= - distributions: sdist bdist_wheel - on: - tags: true diff --git a/README.md b/README.md index e0c7a88..676361c 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Type-safe Monads -[![Build Status](https://travis-ci.com/correl/typesafe-monads.svg?branch=master)](https://travis-ci.com/correl/typesafe-monads) +![Build](https://github.com/correl/typesafe-monads/workflows/Build/badge.svg) [![codecov](https://codecov.io/gh/correl/typesafe-monads/branch/master/graph/badge.svg)](https://codecov.io/gh/correl/typesafe-monads) [![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/ambv/black) @@ -18,6 +18,31 @@ lack of type constraints preventing incorrect usage. I could've attempted to add type annotations to one of those libraries, but building my own is more fun. +This is a fork of the original work by [Correl Roush](http://correl.phoenixinquis.net/) + +I added some utility methods to make it easier to use in my day to day code and better interate with the pythonic style ( ie _List Comprehension_ ) + +## Installation + +There is no *pipy* release ( yet) + +```bash +$ pip install typesafe-monads +``` + +## Curring + +Mixing Higher order functions ( functions that return a function ) with moand is a very common programming style other functional programming languages. +With _curry_ decorator you can transform a function in a _curried_ function: just apss some positional parameters and get back a function with the remaining ones. + +```python +@curry +def power(exp: int, base: int ) -> int: + return math.pow(base, exp) + +square_fn = power(2) # a function that returns the square of the parameter + +``` ## Base Classes @@ -122,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 @@ -146,7 +174,13 @@ failure type `E`. ### List[T] -Represents a sequence of items. +Represents a ordered sequence of items. + +- Also implements `Monoid`. + +### Set[T] + +Represents a unordered sequence of unique items. - Also implements `Monoid`. @@ -159,3 +193,29 @@ Represents an asynchronous action. ### Reader[T] Represents the application of a function to it's argument. + +## Monads as iterable + +It is handy to iterate over some monad contents. *List* is obliviously the first candidate: +```python + +m_list: List[int] = List([1, 2, 4, 9]) +for i in m_list: + ... + +#Or filter with a generator + +evens: List[int] = [k for k in m_list if k % 2 == 0 ] + +``` + +If you want to something to happen just if a *Maybe* monad is defined +```python + +for n in Just("one"): + ... + +``` + +The same apply for *Results* + diff --git a/monads/__init__.py b/monads/__init__.py index a1f7c9e..f14c3a0 100644 --- a/monads/__init__.py +++ b/monads/__init__.py @@ -2,7 +2,10 @@ from .applicative import Applicative from .monad import Monad from .list import List +from .set import Set from .maybe import Maybe, Just, Nothing from .result import Result, Ok, Err from .future import Future from .reader import Reader + +version = "v0.0.15" diff --git a/monads/applicative.py b/monads/applicative.py index dad5ccf..6c13311 100644 --- a/monads/applicative.py +++ b/monads/applicative.py @@ -19,4 +19,5 @@ def pure(cls, value: T) -> Applicative[T]: # pragma: no cover def apply(self, functor: Any) -> Functor[S]: # pragma: no cover raise NotImplementedError - __and__ = lambda other, self: Applicative.apply(self, other) + def __and__(self, other: Any) -> Functor[S]: # pragma: no cover + return Applicative.apply(self, other) diff --git a/monads/future.py b/monads/future.py index 91f1d7d..be66f9d 100644 --- a/monads/future.py +++ b/monads/future.py @@ -54,12 +54,15 @@ def mcons(acc: Future[List[T]], x: Awaitable[T]) -> Future[List[T]]: future: Future[T] = x if isinstance(x, Future) else Future(x) return acc.bind(lambda acc_: future.map(lambda x_: acc_ + [x_])) - empty: Future[List[T]] = cls.pure([]) + empty_list: List[T] = [] + empty: Future[List[T]] = Future.pure(empty_list) return functools.reduce(mcons, xs, empty) def __await__(self): return self.awaitable.__await__() __rshift__ = bind - __and__ = lambda other, self: Future.apply(self, other) + + __and__ = lambda other, self: Future.apply(self, other) # type: ignore + __mul__ = __rmul__ = map diff --git a/monads/list.py b/monads/list.py index 44ceced..2a21519 100644 --- a/monads/list.py +++ b/monads/list.py @@ -1,10 +1,21 @@ from __future__ import annotations from functools import reduce from itertools import chain -from typing import Callable, Iterable, List as _List, TypeVar +from monads import functor +from typing import ( + Callable, + Iterable, + Iterator, + List as _List, + Optional, + TypeVar, + Union, + cast, +) from .monad import Monad from .monoid import Monoidal +from .currying import CurriedBinary, uncurry T = TypeVar("T") S = TypeVar("S") @@ -16,16 +27,21 @@ def pure(cls, value: T) -> List[T]: 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 + def mzero(cls) -> List[T]: + return cls(list()) + @classmethod def sequence(cls, xs: Iterable[List[T]]) -> List[_List[T]]: """Evaluate monadic actions in sequence, collecting results.""" @@ -33,17 +49,48 @@ def sequence(cls, xs: Iterable[List[T]]) -> List[_List[T]]: def mcons(acc: List[_List[T]], x: List[T]) -> List[_List[T]]: return acc.bind(lambda acc_: x.map(lambda x_: acc_ + [x_])) - empty: List[_List[T]] = cls.pure([]) + empty: List[_List[T]] = List.pure([]) return reduce(mcons, xs, empty) - @classmethod - def mzero(cls) -> List[T]: - return cls(list()) + def flatten(self) -> List[T]: + def flat(acc: List[T], element: T) -> List[T]: + if element and isinstance(element, Iterable): + for k in element: + acc = acc.mappend(List([k])) + elif element: + acc = acc.mappend(List([element])) + return acc + + 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.sort(key=key, reverse=reverse) # type: ignore + return List(lst_copy) + + def fold( + self, func: Union[Callable[[S, T], S], CurriedBinary[S, T, S]], base_val: S + ) -> S: + if isinstance(func, CurriedBinary): + functor = uncurry(cast(CurriedBinary, func)) + else: + functor = func + 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 - __and__ = lambda other, self: List.apply(self, other) __mul__ = __rmul__ = map __rshift__ = bind + + def __sizeof__(self) -> int: + return self._value.__sizeof__() + + def __len__(self) -> int: + return len(list(self._value)) + + def __iter__(self) -> Iterator[T]: + return iter(self._value) diff --git a/monads/maybe.py b/monads/maybe.py index 691c77a..c320a97 100644 --- a/monads/maybe.py +++ b/monads/maybe.py @@ -1,6 +1,18 @@ from __future__ import annotations import functools -from typing import Any, Callable, Generic, Iterable, List, Optional, TypeVar +from typing import ( + Any, + Callable, + Generic, + Iterable, + Iterator, + List, + Optional, + Sized, + TypeVar, + Union, + cast, +) from . import result from .monad import Monad from .monoid import Monoid @@ -10,7 +22,7 @@ E = TypeVar("E") -class Maybe(Monad[T]): +class Maybe(Monad[T], Iterable, Sized): def __init__(self) -> None: # pragma: no cover raise NotImplementedError @@ -39,6 +51,12 @@ def apply(self, functor: Maybe[Callable[[T], S]]) -> Maybe[S]: new: Maybe[S] = Nothing() return new + def or_else(self, default: T) -> T: # pragma: no cover + raise NotImplementedError + + def flatten(self) -> Maybe[Any]: # pragma: no cover + raise NotImplementedError # TODO find a more generic signature + @classmethod def sequence(cls, xs: Iterable[Maybe[T]]) -> Maybe[List[T]]: """Evaluate monadic actions in sequence, collecting results.""" @@ -46,7 +64,8 @@ def sequence(cls, xs: Iterable[Maybe[T]]) -> Maybe[List[T]]: def mcons(acc: Maybe[List[T]], x: Maybe[T]) -> Maybe[List[T]]: return acc.bind(lambda acc_: x.map(lambda x_: acc_ + [x_])) - empty: Maybe[List[T]] = cls.pure([]) + empty_list: List[T] = [] + empty: Maybe[List[T]] = Just(empty_list) return functools.reduce(mcons, xs, empty) def withDefault(self, default: T) -> T: @@ -85,8 +104,9 @@ def fromList(self, xs: List[T]) -> Maybe[T]: else: return Nothing() + __and__ = lambda other, self: Maybe.apply(self, other) # type: ignore + __rshift__ = bind - __and__ = lambda other, self: Maybe.apply(self, other) __mul__ = __rmul__ = map @@ -100,6 +120,23 @@ def __eq__(self, other: object): def __repr__(self) -> str: # pragma: no cover return f"" + def __len__(self) -> int: + return 1 + + def __iter__(self) -> Iterator[T]: + one_list: List[T] = [] + one_list.append(self.value) + return iter(one_list) # TODO lazy evaluation + + def or_else(self, default: T) -> T: + return self.value + + def flatten(self) -> Maybe[Any]: + if isinstance(self.value, Maybe): + return cast(Maybe, self.value).flatten() + else: + return Just(self.value) + class Nothing(Maybe[T]): def __init__(self) -> None: @@ -111,6 +148,19 @@ def __eq__(self, other: object): def __repr__(self) -> str: # pragma: no cover return "" + def __len__(self) -> int: + return 0 + + def __iter__(self) -> Iterator[T]: + empty_list: List[T] = [] + return iter(empty_list) + + def or_else(self, default: T) -> T: + return default + + def flatten(self) -> Maybe[Any]: + return self + def maybe(value: T, predicate: Optional[Callable[[T], bool]] = None) -> Maybe[T]: predicate = predicate or (lambda x: x is not None) @@ -126,19 +176,19 @@ def mzero(cls) -> First: 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]]): @@ -147,16 +197,16 @@ def mzero(cls) -> Last: 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 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 @@ def __eq__(self, other: object) -> bool: 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 @@ def mzero(cls) -> Monoidal: 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 @@ def mzero(cls) -> Addition: 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 @@ def mzero(cls) -> Multiplication: 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/reader.py b/monads/reader.py index 2d330ad..c2728aa 100644 --- a/monads/reader.py +++ b/monads/reader.py @@ -10,7 +10,7 @@ Env = TypeVar("Env") F = Callable[[Env], T] - +# TODO Reader Monad Eample class Reader(Monad[T], Generic[Env, T]): def __init__(self, function: F) -> None: update_wrapper(self, function) @@ -58,7 +58,8 @@ def sequence(cls, xs: Iterable[Reader[Env, T]]) -> Reader[Env, List[T]]: def mcons(acc: Reader[Env, List[T]], x: Reader[Env, T]) -> Reader[Env, List[T]]: return acc.bind(lambda acc_: x.map(lambda x_: acc_ + [x_])) - empty: Reader[Env, List[T]] = cls.pure([]) + empty_list: List[T] = [] + empty: Reader[Env, List[T]] = Reader.pure(empty_list) return reduce(mcons, xs, empty) def __eq__(self, other: object): # pragma: no cover @@ -70,6 +71,7 @@ def __repr__(self): # pragma: no cover signature = inspect.signature(self) return f"" + __and__ = lambda other, self: Reader.apply(self, other) # type: ignore + __mul__ = __rmul__ = map __rshift__ = bind - __and__ = lambda other, self: Reader.apply(self, other) diff --git a/monads/result.py b/monads/result.py index ee72e49..37fab14 100644 --- a/monads/result.py +++ b/monads/result.py @@ -60,7 +60,7 @@ def sequence(cls, xs: Iterable[Result[T, E]]) -> Result[List[T], E]: def mcons(acc: Result[List[T], E], x: Result[T, E]) -> Result[List[T], E]: return acc.bind(lambda acc_: x.map(lambda x_: acc_ + [x_])) - empty: Result[List[T], E] = cls.pure([]) + empty: Result[List[T], E] = Result.pure([]) return functools.reduce(mcons, xs, empty) def withDefault(self, default: T) -> T: @@ -89,8 +89,9 @@ def toOptional(self) -> Optional[T]: else: return None + __and__ = lambda other, self: Result.apply(self, other) # type: ignore + __rshift__ = bind - __and__ = lambda other, self: Result.apply(self, other) __mul__ = __rmul__ = map diff --git a/monads/set.py b/monads/set.py new file mode 100644 index 0000000..498b42b --- /dev/null +++ b/monads/set.py @@ -0,0 +1,107 @@ +from __future__ import annotations +from functools import reduce +from itertools import chain +from monads import functor, List +from typing import ( + Any, + Callable, + Iterable, + Iterator, + Set as _Set, + List as _List, + Optional, + TypeVar, + Union, + cast, +) + +from .monad import Monad +from .monoid import Monoidal +from .currying import CurriedBinary, uncurry + +T = TypeVar("T") +S = TypeVar("S") + + +class Set(Monad[T], Monoidal[set]): + @classmethod + def pure(cls, value: T) -> Set[T]: + def unpack(k: T) -> set: + s: set = set() + if isinstance(k, Iterable): + for v in k: + s.union(unpack(v)) + else: + s.add(k) + return s + + return Set(unpack(value)) + + def bind(self, function: Callable[[T], Set[S]]) -> Set[S]: + 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))) + + 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])) + ) + + @classmethod + def mzero(cls) -> Set[T]: + return cls(set()) + + @classmethod + def sequence(cls, xs: Iterable[Set[T]]) -> Set[_List[T]]: + """Evaluate monadic actions in sequence, collecting results.""" + + def mcons(acc: Set[_Set[T]], x: Set[T]) -> Set[_Set[T]]: + return acc.bind(lambda acc_: x.map(lambda x_: acc_.union(set([x_])))) + + empty: Set[_Set[T]] = Set.pure(set()) + return Set(set(reduce(mcons, xs, empty))) # type: ignore + + def flatten(self) -> Set[T]: + def flat(acc: Set[T], element: T) -> Set[T]: + if element and isinstance(element, Iterable): + for k in element: + acc = acc.mappend(Set(set([k]))) + elif element: + acc = acc.mappend(Set(set([element]))) + return acc + + 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.sort(key=key, reverse=reverse) # type: ignore + return Set(lst_copy) + + def fold( + self, func: Union[Callable[[S, T], S], CurriedBinary[S, T, S]], base_val: S + ) -> S: + if isinstance(func, CurriedBinary): + functor = uncurry(cast(CurriedBinary, func)) + else: + functor = func + 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)) + + __add__ = mappend + __mul__ = __rmul__ = map + __rshift__ = bind + + def __sizeof__(self) -> int: + return self._value.__sizeof__() + + def __len__(self) -> int: + return len(set(self._value)) + + def __iter__(self) -> Iterator[T]: + return iter(self._value) diff --git a/pyproject.toml b/pyproject.toml index a7c75bf..03b5fc0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -9,5 +9,6 @@ exclude = ''' | \.mypy_cache | build | dist + | env )/ ''' diff --git a/setup.cfg b/setup.cfg index a8f0911..0536733 100644 --- a/setup.cfg +++ b/setup.cfg @@ -3,3 +3,6 @@ test=pytest [tool:pytest] addopts = --black --mypy --cov monads --cov-report xml:reports/coverage.xml + +[mypy-tests] +ignore_errors = True \ No newline at end of file diff --git a/setup.py b/setup.py index bd17ff4..ba194de 100644 --- a/setup.py +++ b/setup.py @@ -6,9 +6,9 @@ setup( name="typesafe-monads", - version="0.8", - author="Correl Roush", - author_email="correl@gmail.com", + version="v0.0.15", + author="Correl Roush, Sam Reghenzi", + author_email="correl@gmail.com, sammyrulez@gmail.com", description="Type-annotated monad implementations for Python 3.7+", long_description=long_description, long_description_content_type="text/markdown", diff --git a/tests/fixtures.py b/tests/fixtures.py index 79936cf..4caf50c 100644 --- a/tests/fixtures.py +++ b/tests/fixtures.py @@ -1,8 +1,8 @@ import pytest # type: ignore from typing import Type -from monads import Maybe, List, Result +from monads import Maybe, List, Result, Set -@pytest.fixture(scope="module", params=[Maybe, List, Result]) +@pytest.fixture(scope="module", params=[Maybe, List, Result, Set]) def monad(request) -> Type: return request.param diff --git a/tests/test_applicatives.py b/tests/test_applicatives.py index 01f6186..e16cdf3 100644 --- a/tests/test_applicatives.py +++ b/tests/test_applicatives.py @@ -18,4 +18,5 @@ def test_apply_and_operator(monad) -> None: subtract: Callable[[int], Callable[[int], int]] = lambda x: lambda y: x - y ten = monad.pure(10) six = monad.pure(6) - assert six.apply(ten.map(subtract)) == subtract * ten & six + functor = ten.map(subtract) + assert six.apply(functor) == subtract * ten & six diff --git a/tests/test_currying.py b/tests/test_currying.py index fd3e27b..e46aa22 100644 --- a/tests/test_currying.py +++ b/tests/test_currying.py @@ -28,12 +28,15 @@ def test_curried_function_annotation_drops_arguments_as_it_is_applied() -> None: def add3(a: int, b: int, c: int) -> int: return a + b + c - assert inspect.Signature( - [ - inspect.Parameter( - param, inspect.Parameter.POSITIONAL_OR_KEYWORD, annotation=int - ) - for param in ["b", "c"] - ], - return_annotation=int, - ) == inspect.signature(curry(add3)(1)) + assert ( + inspect.Signature( + [ + inspect.Parameter( + param, inspect.Parameter.POSITIONAL_OR_KEYWORD, annotation=int + ) + for param in ["b", "c"] + ], + return_annotation=int, + ) + == inspect.signature(curry(add3)(1)) + ) diff --git a/tests/test_list.py b/tests/test_list.py new file mode 100644 index 0000000..9483159 --- /dev/null +++ b/tests/test_list.py @@ -0,0 +1,37 @@ +from typing import Any, Union +import pytest # type: ignore +from monads import List +from monads.currying import curry + + +def test_fold() -> None: + m_list: List[int] = List([1, 2, 4]) + total: int = m_list.fold(lambda k, h: k + h, 0) + assert total == 7 + + @curry + def to_be_curried(offset: int, h: int, k: int) -> int: + return offset + h + k + + curried_total: int = m_list.fold(to_be_curried(1), 0) + assert curried_total == 10 + + +def test_flatten() -> None: + m_list: List[Union[int, List[int]]] = List([1, 2, List([3, 4])]) + assert len(m_list.flatten()) == 4 + + +def test_loop() -> None: + m_list: List[int] = List([1, 2, 4]) + x = 0 + for i in m_list: + x = x + 1 + assert x == 3 + + +def test_len() -> None: + m_list: List[int] = List([1, 2, 4]) + assert len(m_list) == 3 + assert m_list + assert not List.mzero() diff --git a/tests/test_maybe.py b/tests/test_maybe.py index 1d3140b..a41c3f6 100644 --- a/tests/test_maybe.py +++ b/tests/test_maybe.py @@ -126,3 +126,35 @@ def test_just_to_optional() -> None: def test_nothing_to_optional() -> None: assert None == Nothing().toOptional() + + +def test_as_iterable() -> None: + m_empty: Maybe[str] = Nothing() + i = 0 + for n in m_empty: + i = i + 1 + assert i == 0 + + for n in Just("one"): + i = i + 1 + assert i == 1 + + assert len(m_empty) == 0 + assert len(Just("one")) == 1 + + +def test_or_else() -> None: + m_empty: Maybe[str] = Nothing() + assert m_empty.or_else("backup") == "backup" + + m_full: Maybe[str] = Just("becon") + assert m_full.or_else("backup") == "becon" + + +def test_flatten() -> None: + m_empty: Maybe[Maybe[str]] = Just(Nothing()) + m_flat: Maybe[str] = m_empty.flatten() + assert m_flat.or_else("backup") == "backup" + + m_full: Maybe[Maybe[str]] = Just(Just("becon")) + assert m_full.flatten().or_else("backup") == "becon" diff --git a/tests/test_monoids.py b/tests/test_monoids.py index 927a11c..c5e19bb 100644 --- a/tests/test_monoids.py +++ b/tests/test_monoids.py @@ -55,3 +55,12 @@ 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) + 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 + assert "can't set attribute" in str(excinfo.value) diff --git a/tests/test_set.py b/tests/test_set.py new file mode 100644 index 0000000..2cbb2e6 --- /dev/null +++ b/tests/test_set.py @@ -0,0 +1,37 @@ +from typing import Any, Union +import pytest # type: ignore +from monads import Set +from monads.currying import curry + + +def test_fold() -> None: + m_set: Set[int] = Set(set([1, 2, 4])) + total: int = m_set.fold(lambda k, h: k + h, 0) + assert total == 7 + + @curry + def to_be_curried(offset: int, h: int, k: int) -> int: + return offset + h + k + + curried_total: int = m_set.fold(to_be_curried(1), 0) + assert curried_total == 10 + + +def test_flatten() -> None: + m_set: Set[Union[int, Set[int]]] = Set(set([1, 2, (3, 4)])) + assert len(m_set.flatten()) == 4 + + +def test_loop() -> None: + m_set: Set[int] = Set(set([1, 2, 4])) + x = 0 + for i in m_set: + x = x + 1 + assert x == 3 + + +def test_len() -> None: + m_set: Set[int] = Set(set([1, 2, 4])) + assert len(m_set) == 3 + assert m_set + assert not Set.mzero()