diff --git a/README.md b/README.md index cff4667..e0c7a88 100644 --- a/README.md +++ b/README.md @@ -150,6 +150,12 @@ Represents a sequence of items. - Also implements `Monoid`. +### Future[T] + +Represents an asynchronous action. + +- Also implements `Awaitable`. + ### Reader[T] Represents the application of a function to it's argument. diff --git a/monads/__init__.py b/monads/__init__.py index bff6eac..a1f7c9e 100644 --- a/monads/__init__.py +++ b/monads/__init__.py @@ -4,4 +4,5 @@ from .monad import Monad from .list import List from .maybe import Maybe, Just, Nothing from .result import Result, Ok, Err +from .future import Future from .reader import Reader diff --git a/monads/future.py b/monads/future.py new file mode 100644 index 0000000..63df0b1 --- /dev/null +++ b/monads/future.py @@ -0,0 +1,58 @@ +from __future__ import annotations +import asyncio +from typing import Awaitable, Callable, TypeVar, Union +from .monad import Monad + +T = TypeVar("T") +S = TypeVar("S") + + +class Future(Monad[T]): + """Wraps an Awaitable in a Monad. + + The resulting Future object is, itself, Awaitable. + """ + + def __init__(self, awaitable: Awaitable[T]) -> None: + self.awaitable = awaitable + + @classmethod + def pure(cls, value: Union[T, Awaitable[T]]) -> Future[T]: + if isinstance(value, Awaitable): + return Future(value) + else: + + async def identity(x: T) -> T: + return x + + return Future(identity(value)) + + def map(self, function: Callable[[T], S]) -> Future[S]: + async def map(f: Callable[[T], S], x: Awaitable[T]) -> S: + x_ = await x + return f(x_) + + return Future(map(function, self.awaitable)) + + def apply(self, functor: Awaitable[Callable[[T], S]]) -> Future[S]: + async def apply(f: Awaitable[Callable[[T], S]], x: Awaitable[T]) -> S: + f_ = await f + x_ = await x + return f_(x_) + + return Future(apply(functor, self.awaitable)) + + def bind(self, function: Callable[[T], Awaitable[S]]) -> Future[S]: + async def bind(f: Callable[[T], Awaitable[S]], x: Awaitable[T]) -> S: + x_ = await x + y = await function(x_) + return y + + return Future(bind(function, self.awaitable)) + + def __await__(self): + return self.awaitable.__await__() + + __rshift__ = bind + __and__ = lambda other, self: Future.apply(self, other) + __mul__ = __rmul__ = map diff --git a/tests/fixtures.py b/tests/fixtures.py index 79936cf..d27cda1 100644 --- a/tests/fixtures.py +++ b/tests/fixtures.py @@ -1,6 +1,6 @@ import pytest # type: ignore from typing import Type -from monads import Maybe, List, Result +from monads import Maybe, List, Result, Future @pytest.fixture(scope="module", params=[Maybe, List, Result]) diff --git a/tests/test_future.py b/tests/test_future.py new file mode 100644 index 0000000..43858c8 --- /dev/null +++ b/tests/test_future.py @@ -0,0 +1,93 @@ +import pytest # type: ignore +from typing import Any, Callable, TypeVar + +from monads import Functor, Applicative, Future +from monads.reader import curry + +T = TypeVar("T") +S = TypeVar("S") + + +@pytest.mark.asyncio +async def test_pure_accepts_values_or_awaitables() -> None: + async def three() -> int: + return 3 + + a: Future[int] = Future.pure(3) + b: Future[int] = Future.pure(three()) + assert await a == await b + + +@pytest.mark.asyncio +async def test_functor_identity() -> None: + identity: Callable[[int], int] = lambda x: x + assert await Future.pure(3) == await Future.pure(3).map(identity) + + +@pytest.mark.asyncio +async def test_functor_associativity() -> None: + f: Callable[[int], int] = lambda x: x + 1 + g: Callable[[int], str] = lambda x: str(x) + assert await Future.pure(3).map(lambda x: g(f(x))) == await Future.pure(3).map( + f + ).map(g) + + +@pytest.mark.asyncio +async def test_functor_map_mul_operator() -> None: + identity: Callable[[int], int] = lambda x: x + assert await Future.pure(3).map(identity) == await (Future.pure(3) * identity) + + +@pytest.mark.asyncio +async def test_functor_map_rmul_operator() -> None: + identity: Callable[[int], int] = lambda x: x + assert await Future.pure(3).map(identity) == await (identity * Future.pure(3)) + + +@pytest.mark.asyncio +async def test_applicative_fmap_using_ap() -> None: + f: Callable[[int], int] = lambda x: x + 1 + assert await Future.pure(3).map(f) == await Future.pure(3).apply(Future.pure(f)) + + +@pytest.mark.asyncio +async def test_monad_bind() -> None: + expected: Future[int] = Future.pure(2) + m: Future[int] = Future.pure(1) + assert await expected == await m.bind(lambda x: Future.pure(x + 1)) + + +@pytest.mark.asyncio +async def test_monad_bind_rshift_operator() -> None: + f: Callable[[int], Future[int]] = lambda x: Future.pure(x + 1) + assert await Future.pure(2).bind(f) == await (Future.pure(2) >> f) + + +@pytest.mark.asyncio +async def test_monad_left_identity() -> None: + n: int = 3 + + def f(n: int) -> Future[int]: + return Future.pure(n * 3) + + m: Future[int] = Future.pure(n) + assert await m.bind(f) == await f(n) + + +@pytest.mark.asyncio +async def test_monad_right_identity() -> None: + assert await Future.pure(3) == await Future.pure(3).bind(lambda x: Future.pure(x)) + + +@pytest.mark.asyncio +async def test_monad_associativity() -> None: + def f(n: int) -> Future[int]: + return Future.pure(n * 3) + + def g(n: int) -> Future[int]: + return Future.pure(n + 5) + + assert await Future.pure(3).bind(f).bind(g) == await Future.pure(3).bind( + lambda x: f(x).bind(g) + )