Module pynction.monads.either
Expand source code
import abc
import functools
from dataclasses import dataclass
from typing import Any, Callable, Generator, Generic, TypeVar, cast
from typing_extensions import ParamSpec
L = TypeVar("L", covariant=True)
L1 = TypeVar("L1")
R = TypeVar("R", covariant=True)
R1 = TypeVar("R1")
class Either(abc.ABC, Generic[L, R]):
"""
Either monad is an abstraction of a result that could be 2 things.
It's super useful for error handling, by "standard" the left value
is used for errors and the right value is used for successful results.
[Either in Haskell](http://learnyouahaskell.com/for-a-few-monads-more#error)
"""
@staticmethod
def right(value: R) -> "Either[Any, R]": # type: ignore
"""
Creates a `Right` instance with the given type
"""
return Right(value)
@staticmethod
def left(value: L) -> "Either[L, Any]": # type: ignore
"""
Creates a `Left` instance with the given type
"""
return Left(value)
@abc.abstractproperty
def is_left(self) -> bool:
"""
Checks if the current instance is a `Left` instance
"""
raise NotImplementedError
@abc.abstractproperty
def is_right(self) -> bool:
"""
Checks if the current instance is a `Right` instance
"""
raise NotImplementedError
@abc.abstractmethod
def map(self, f: Callable[[R], R1]) -> "Either[L, R1]":
"""
Applies `f` over the right value. If the instance is a `Left`
the function `f` is ignored.
"""
raise NotImplementedError
@abc.abstractmethod
def filter_or_else(
self, predicate: Callable[[R], bool], left_value: L # type: ignore
) -> "Either[L, R]":
"""
Evaluate the `predicate` over the right value, if the result is
`True` the returned either is the same `Right` instance but if the
predicate is not satisfied then the result is a `Left` instance with
the `left_value`
"""
raise NotImplementedError
@abc.abstractmethod
def get_or_else_get(self, f: Callable[[L], R]) -> R:
"""
It returns the right value if the instance is `Right`
but if the instance is a `Left` it applies the `f` function and return
the result obtained by that function.
"""
raise NotImplementedError
@dataclass(frozen=True)
class Right(Either[Any, R]):
_value: R
def __str__(self) -> str:
return f"Right[{self._value}]"
@property
def is_left(self) -> bool:
return False
@property
def is_right(self) -> bool:
return True
def map(self, f: Callable[[R], R1]) -> Either[L, R1]:
return Right(f(self._value))
def filter_or_else(
self, satisfyCondition: Callable[[R], bool], leftValue: L # type: ignore
) -> Either[L, R]:
if satisfyCondition(self._value):
return self
else:
return Left(leftValue)
def get_or_else_get(self, _: Callable[[L], R]) -> R:
return self._value
@dataclass(frozen=True)
class Left(Either[L, Any]):
_value: L
def __str__(self) -> str:
return f"Left[{self._value}]"
@property
def is_left(self) -> bool:
return True
@property
def is_right(self) -> bool:
return False
def map(self, _: Callable[[R], R1]) -> Either[L, R1]:
return Left(self._value)
def filter_or_else(self, _: Callable[[R], bool], _1: L) -> Either[L, R]: # type: ignore
return self
def get_or_else_get(self, f: Callable[[L], R]) -> R:
return f(self._value)
P = ParamSpec("P")
DoEither = Generator[Either[L, R], R, R1]
"""
`DoEither[L, R, R1]`
This type must be used with the `@do_either` decorator.
This type just reflects the either value that is processed by the decorator.
So the L and R represents the either value that the do_either receives
and R1 is the value that is returned by the function.
Example usage:
1. The `@do_either` receives an Either of type `Either[str, User]` and then returns an `Either[str, None]`
```
@do_either
def example1(id: int) -> DoEither[str, User, None]:
user = yield find_user(id).to_either("USER_NOT_FOUND")
user = yield execute_validation(user)
yield execute_use_case(user)
return None
```
"""
DoEitherN = DoEither[L, Any, R1]
"""
`DoEitherN[L, R1]`
This type should be used when the function that has the `do_either` decorator provides
more than one type for `R`. For this type you only need to provide the left type
that the function will return (`L`) and the right value that will result after executing the function (`R1`).
In order to be able to infer the different types that your function provides
you need to use a special syntax. This syntax is the conjunction of `yield from` + `_e` helper.
Example:
```
@do_either
def example1(id: int) -> DoEitherN[str, User]:
name = yield from _e(obtain_name()) # mypy will infer "name" is str
age = yield from _e(obtain_age()) # mypy will infer that "age" is int
return User(name, age)
```
"""
def _(obj: Either[L, R]) -> DoEitherN[L, R]:
"""
This helper should be used along with `DoEitherN` type. This helper
is a workaround to allow dynamic typing in a do notation context.
It should be used with `yield from`
Example:
```
@do_either
def example1(id: int) -> DoEitherN[str, User]:
name = yield from _e(obtain_name()) # mypy will infer "name" is str
age = yield from _e(obtain_age()) # mypy will infer that "age" is int
return User(name, age)
```
"""
a = yield obj
return a
def do(generator: Callable[P, DoEither[L, R, R1]]) -> Callable[P, Either[L, R1]]:
"""
`@do_either` is a decorator that enables the decorated function to support `do` notation
like Haskell. [Do notation in Haskell](http://learnyouahaskell.com/a-fistful-of-monads#do-notation)
To enable this functionality you must `yield` your either value so that the decorator function
can have control over your flow.
Example usage:
1. The `@do_either` receives an Either of type `Either[str, User]` and then returns an `Either[str, None]`
1.1 If the `@do_either` receives a `left` then the flow is cut exactly in that point
returning a Left[str] in this case
1.2 If the `@do_either` receives a `right` the decorator just return the value
inside of it so then your function can use it to perform anything on it
```
@do_either
def example1(id: int) -> DoEither[str, User, None]:
user = yield find_user(id).to_either("USER_NOT_FOUND")
user = yield execute_validation(user)
yield execute_use_case(user)
return None
```
Example with dynamic typing
```
@do_either
def example1(id: int) -> DoEitherN[str, User]:
name = yield from _e(obtain_name()) # mypy will infer "name" is str
age = yield from _e(obtain_age()) # mypy will infer that "age" is int
return User(name, age)
```
"""
def wrapper(*args: P.args, **kwargs: P.kwargs) -> Either[L, R1]:
gen = generator(*args, **kwargs)
either_monad = next(gen)
while True:
try:
if type(either_monad) == Left:
return either_monad
either_monad = gen.send(cast(Right, either_monad)._value)
except StopIteration as e:
return Right(e.value)
functools.update_wrapper(wrapper, generator)
return wrapper
Global variables
var DoEither-
DoEither[L, R, R1]This type must be used with the
@do_eitherdecorator. This type just reflects the either value that is processed by the decorator. So the L and R represents the either value that the do_either receives and R1 is the value that is returned by the function.Example usage: 1. The
@do_eitherreceives an Either of typeEither[str, User]and then returns anEither[str, None]@do_either def example1(id: int) -> DoEither[str, User, None]: user = yield find_user(id).to_either("USER_NOT_FOUND") user = yield execute_validation(user) yield execute_use_case(user) return None var DoEitherN-
DoEitherN[L, R1]This type should be used when the function that has the
do_eitherdecorator provides more than one type forR. For this type you only need to provide the left type that the function will return (L) and the right value that will result after executing the function (R1).In order to be able to infer the different types that your function provides you need to use a special syntax. This syntax is the conjunction of
yield from+_ehelper.Example:
@do_either def example1(id: int) -> DoEitherN[str, User]: name = yield from _e(obtain_name()) # mypy will infer "name" is str age = yield from _e(obtain_age()) # mypy will infer that "age" is int return User(name, age)
Functions
def do(generator: Callable[[~P], Generator[Either[+L, +R], +R, ~R1]]) ‑> Callable[[~P], Either[+L, ~R1]]-
@do_eitheris a decorator that enables the decorated function to supportdo()notation like Haskell. Do notation in HaskellTo enable this functionality you must
yieldyour either value so that the decorator function can have control over your flow.Example usage: 1. The
@do_eitherreceives an Either of typeEither[str, User]and then returns anEither[str, None]1.1 If the `@do_either` receives a <code>left</code> then the flow is cut exactly in that point returning a Left[str] in this case 1.2 If the `@do_either` receives a <code>right</code> the decorator just return the value inside of it so then your function can use it to perform anything on it@do_either def example1(id: int) -> DoEither[str, User, None]: user = yield find_user(id).to_either("USER_NOT_FOUND") user = yield execute_validation(user) yield execute_use_case(user) return NoneExample with dynamic typing
@do_either def example1(id: int) -> DoEitherN[str, User]: name = yield from _e(obtain_name()) # mypy will infer "name" is str age = yield from _e(obtain_age()) # mypy will infer that "age" is int return User(name, age)Expand source code
def do(generator: Callable[P, DoEither[L, R, R1]]) -> Callable[P, Either[L, R1]]: """ `@do_either` is a decorator that enables the decorated function to support `do` notation like Haskell. [Do notation in Haskell](http://learnyouahaskell.com/a-fistful-of-monads#do-notation) To enable this functionality you must `yield` your either value so that the decorator function can have control over your flow. Example usage: 1. The `@do_either` receives an Either of type `Either[str, User]` and then returns an `Either[str, None]` 1.1 If the `@do_either` receives a `left` then the flow is cut exactly in that point returning a Left[str] in this case 1.2 If the `@do_either` receives a `right` the decorator just return the value inside of it so then your function can use it to perform anything on it ``` @do_either def example1(id: int) -> DoEither[str, User, None]: user = yield find_user(id).to_either("USER_NOT_FOUND") user = yield execute_validation(user) yield execute_use_case(user) return None ``` Example with dynamic typing ``` @do_either def example1(id: int) -> DoEitherN[str, User]: name = yield from _e(obtain_name()) # mypy will infer "name" is str age = yield from _e(obtain_age()) # mypy will infer that "age" is int return User(name, age) ``` """ def wrapper(*args: P.args, **kwargs: P.kwargs) -> Either[L, R1]: gen = generator(*args, **kwargs) either_monad = next(gen) while True: try: if type(either_monad) == Left: return either_monad either_monad = gen.send(cast(Right, either_monad)._value) except StopIteration as e: return Right(e.value) functools.update_wrapper(wrapper, generator) return wrapper
Classes
class Either (*args, **kwds)-
Either monad is an abstraction of a result that could be 2 things. It's super useful for error handling, by "standard" the left value is used for errors and the right value is used for successful results. Either in Haskell
Expand source code
class Either(abc.ABC, Generic[L, R]): """ Either monad is an abstraction of a result that could be 2 things. It's super useful for error handling, by "standard" the left value is used for errors and the right value is used for successful results. [Either in Haskell](http://learnyouahaskell.com/for-a-few-monads-more#error) """ @staticmethod def right(value: R) -> "Either[Any, R]": # type: ignore """ Creates a `Right` instance with the given type """ return Right(value) @staticmethod def left(value: L) -> "Either[L, Any]": # type: ignore """ Creates a `Left` instance with the given type """ return Left(value) @abc.abstractproperty def is_left(self) -> bool: """ Checks if the current instance is a `Left` instance """ raise NotImplementedError @abc.abstractproperty def is_right(self) -> bool: """ Checks if the current instance is a `Right` instance """ raise NotImplementedError @abc.abstractmethod def map(self, f: Callable[[R], R1]) -> "Either[L, R1]": """ Applies `f` over the right value. If the instance is a `Left` the function `f` is ignored. """ raise NotImplementedError @abc.abstractmethod def filter_or_else( self, predicate: Callable[[R], bool], left_value: L # type: ignore ) -> "Either[L, R]": """ Evaluate the `predicate` over the right value, if the result is `True` the returned either is the same `Right` instance but if the predicate is not satisfied then the result is a `Left` instance with the `left_value` """ raise NotImplementedError @abc.abstractmethod def get_or_else_get(self, f: Callable[[L], R]) -> R: """ It returns the right value if the instance is `Right` but if the instance is a `Left` it applies the `f` function and return the result obtained by that function. """ raise NotImplementedErrorAncestors
- abc.ABC
- typing.Generic
Subclasses
Static methods
def left(value: +L) ‑> Either[+L, typing.Any]-
Creates a
Leftinstance with the given typeExpand source code
@staticmethod def left(value: L) -> "Either[L, Any]": # type: ignore """ Creates a `Left` instance with the given type """ return Left(value) def right(value: +R) ‑> Either[typing.Any, +R]-
Creates a
Rightinstance with the given typeExpand source code
@staticmethod def right(value: R) -> "Either[Any, R]": # type: ignore """ Creates a `Right` instance with the given type """ return Right(value)
Instance variables
var is_left : bool-
Checks if the current instance is a
LeftinstanceExpand source code
@abc.abstractproperty def is_left(self) -> bool: """ Checks if the current instance is a `Left` instance """ raise NotImplementedError var is_right : bool-
Checks if the current instance is a
RightinstanceExpand source code
@abc.abstractproperty def is_right(self) -> bool: """ Checks if the current instance is a `Right` instance """ raise NotImplementedError
Methods
def filter_or_else(self, predicate: Callable[[+R], bool], left_value: +L) ‑> Either[+L, +R]-
Evaluate the
predicateover the right value, if the result isTruethe returned either is the sameRightinstance but if the predicate is not satisfied then the result is aLeftinstance with theleft_valueExpand source code
@abc.abstractmethod def filter_or_else( self, predicate: Callable[[R], bool], left_value: L # type: ignore ) -> "Either[L, R]": """ Evaluate the `predicate` over the right value, if the result is `True` the returned either is the same `Right` instance but if the predicate is not satisfied then the result is a `Left` instance with the `left_value` """ raise NotImplementedError def get_or_else_get(self, f: Callable[[+L], +R]) ‑> +R-
It returns the right value if the instance is
Rightbut if the instance is aLeftit applies theffunction and return the result obtained by that function.Expand source code
@abc.abstractmethod def get_or_else_get(self, f: Callable[[L], R]) -> R: """ It returns the right value if the instance is `Right` but if the instance is a `Left` it applies the `f` function and return the result obtained by that function. """ raise NotImplementedError def map(self, f: Callable[[+R], ~R1]) ‑> Either[+L, ~R1]-
Applies
fover the right value. If the instance is aLeftthe functionfis ignored.Expand source code
@abc.abstractmethod def map(self, f: Callable[[R], R1]) -> "Either[L, R1]": """ Applies `f` over the right value. If the instance is a `Left` the function `f` is ignored. """ raise NotImplementedError
class Left (_value: +L)-
Left(args, *kwds)
Expand source code
class Left(Either[L, Any]): _value: L def __str__(self) -> str: return f"Left[{self._value}]" @property def is_left(self) -> bool: return True @property def is_right(self) -> bool: return False def map(self, _: Callable[[R], R1]) -> Either[L, R1]: return Left(self._value) def filter_or_else(self, _: Callable[[R], bool], _1: L) -> Either[L, R]: # type: ignore return self def get_or_else_get(self, f: Callable[[L], R]) -> R: return f(self._value)Ancestors
- Either
- abc.ABC
- typing.Generic
Inherited members
class Right (_value: +R)-
Right(args, *kwds)
Expand source code
class Right(Either[Any, R]): _value: R def __str__(self) -> str: return f"Right[{self._value}]" @property def is_left(self) -> bool: return False @property def is_right(self) -> bool: return True def map(self, f: Callable[[R], R1]) -> Either[L, R1]: return Right(f(self._value)) def filter_or_else( self, satisfyCondition: Callable[[R], bool], leftValue: L # type: ignore ) -> Either[L, R]: if satisfyCondition(self._value): return self else: return Left(leftValue) def get_or_else_get(self, _: Callable[[L], R]) -> R: return self._valueAncestors
- Either
- abc.ABC
- typing.Generic
Inherited members