mirror of
https://github.com/correl/typesafe-monads.git
synced 2025-04-05 09:12:36 -09:00
Merge d046141d8d
into 078dd46faa
This commit is contained in:
commit
89d2d46ae5
24 changed files with 552 additions and 73 deletions
76
.github/workflows/build.yaml
vendored
Normal file
76
.github/workflows/build.yaml
vendored
Normal file
|
@ -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 }}
|
||||
|
||||
|
||||
|
||||
|
22
.github/workflows/todo.yaml
vendored
Normal file
22
.github/workflows/todo.yaml
vendored
Normal file
|
@ -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"
|
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -109,3 +109,4 @@ venv.bak/
|
|||
.mypy_cache/
|
||||
.dmypy.json
|
||||
dmypy.json
|
||||
.vscode
|
||||
|
|
21
.travis.yml
21
.travis.yml
|
@ -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
|
64
README.md
64
README.md
|
@ -1,6 +1,6 @@
|
|||
# Type-safe Monads
|
||||
|
||||
[](https://travis-ci.com/correl/typesafe-monads)
|
||||

|
||||
[](https://codecov.io/gh/correl/typesafe-monads)
|
||||
[](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*
|
||||
|
||||
|
|
|
@ -2,7 +2,10 @@ from .functor import Functor
|
|||
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"
|
||||
|
|
|
@ -19,4 +19,5 @@ class Applicative(Functor[T]):
|
|||
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)
|
||||
|
|
|
@ -54,12 +54,15 @@ class Future(Monad[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
|
||||
|
|
|
@ -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 @@ 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
|
||||
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 @@ class List(Monad[T], Monoidal[list]):
|
|||
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)
|
||||
|
|
|
@ -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 @@ S = TypeVar("S")
|
|||
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 @@ class Maybe(Monad[T]):
|
|||
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 @@ class Maybe(Monad[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 @@ class Maybe(Monad[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 @@ class Just(Maybe[T]):
|
|||
def __repr__(self) -> str: # pragma: no cover
|
||||
return f"<Just {self.value}>"
|
||||
|
||||
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 @@ class Nothing(Maybe[T]):
|
|||
def __repr__(self) -> str: # pragma: no cover
|
||||
return "<Nothing>"
|
||||
|
||||
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 @@ 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"<First {self.value}>"
|
||||
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
|
||||
return First.mconcat(map(lambda x: First(x), xs))._value
|
||||
|
||||
|
||||
class Last(Monoid[Maybe[T]]):
|
||||
|
@ -147,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"<Last {self.value}>"
|
||||
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
|
||||
return Last.mconcat(map(lambda x: Last(x), xs))._value
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -10,7 +10,7 @@ S = TypeVar("S")
|
|||
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 @@ class Reader(Monad[T], Generic[Env, 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 @@ class Reader(Monad[T], Generic[Env, T]):
|
|||
signature = inspect.signature(self)
|
||||
return f"<Reader {module}.{name}{signature}>"
|
||||
|
||||
__and__ = lambda other, self: Reader.apply(self, other) # type: ignore
|
||||
|
||||
__mul__ = __rmul__ = map
|
||||
__rshift__ = bind
|
||||
__and__ = lambda other, self: Reader.apply(self, other)
|
||||
|
|
|
@ -60,7 +60,7 @@ class Result(Monad[T], Generic[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 @@ class Result(Monad[T], Generic[T, E]):
|
|||
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
|
||||
|
||||
|
||||
|
|
107
monads/set.py
Normal file
107
monads/set.py
Normal file
|
@ -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)
|
|
@ -9,5 +9,6 @@ exclude = '''
|
|||
| \.mypy_cache
|
||||
| build
|
||||
| dist
|
||||
| env
|
||||
)/
|
||||
'''
|
||||
|
|
|
@ -3,3 +3,6 @@ test=pytest
|
|||
|
||||
[tool:pytest]
|
||||
addopts = --black --mypy --cov monads --cov-report xml:reports/coverage.xml
|
||||
|
||||
[mypy-tests]
|
||||
ignore_errors = True
|
6
setup.py
6
setup.py
|
@ -6,9 +6,9 @@ with open("README.md", "r") as f:
|
|||
|
||||
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",
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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))
|
||||
)
|
||||
|
|
37
tests/test_list.py
Normal file
37
tests/test_list.py
Normal file
|
@ -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()
|
|
@ -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"
|
||||
|
|
|
@ -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)
|
||||
|
|
37
tests/test_set.py
Normal file
37
tests/test_set.py
Normal file
|
@ -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()
|
Loading…
Add table
Reference in a new issue