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_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
var DoEitherN

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)

Functions

def do(generator: Callable[[~P], Generator[Either[+L, +R], +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

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 <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 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)
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 NotImplementedError

Ancestors

  • abc.ABC
  • typing.Generic

Subclasses

Static methods

def left(value: +L) ‑> Either[+L, typing.Any]

Creates a Left instance with the given type

Expand 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 Right instance with the given type

Expand 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 Left instance

Expand 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 Right instance

Expand 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 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

Expand 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 Right but if the instance is a Left it applies the f function 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 f over the right value. If the instance is a Left the function f is 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._value

Ancestors

  • Either
  • abc.ABC
  • typing.Generic

Inherited members