Skip to content

either

The either package provides both functions and an Either class, that allows both method chaining and do notation style syntax.

Either[L, R]

Either[L, R] is roughly defined as:

class Left(Generic[L]):
  value: L
  tag: Literal['left']

class Right(Generic[R]):
  value: R
  tag: Literal['right']

Either[L, R] = Left[L] | Right[R]

Do Notation

Thought it supports both method chaining and either.tag == 'left' branching, the most common way to use an either is with a sort of do notation:

from haskellian import either as E, Either

def fetch_this() -> Either[KeyError, str]:
  ...

def fetch_that() -> Either[ValueError, str]:
  ...

@E.do[KeyError|ValueError]()
def do_function() -> str:
  this = fetch_this().unwrap() # str
  that = fetch_that().unwrap() # str
  return f'{this} and {that}'

do_function() # Either[KeyError|ValueError, str]

If you're familiar with Rust's ? operator, this should be familiar.

Functions

The either module has just a few utility functions:

safe

Bases: Generic[Err]

A decorator to catch exceptions and return them as Left.

@E.safe[OSError]()
def safe_write(path: str, content: bytes):
  with open(path, 'wb') as f:
    f.write(content)

result = safe_write('file.txt', b'content') # Either[OSError, None]
Source code in haskellian/src/haskellian/either/funcs.py
class safe(Generic[Err]):
  """A decorator to catch exceptions and return them as `Left`.

  ```python
  @E.safe[OSError]()
  def safe_write(path: str, content: bytes):
    with open(path, 'wb') as f:
      f.write(content)

  result = safe_write('file.txt', b'content') # Either[OSError, None]
  ```
  """

  @property
  def exc(self) -> type[Err]:
    return get_args(self.__orig_class__)[0] # type: ignore (yep, typing internals are messed up)

  @overload
  def __call__(self, fn: Callable[P, Coroutine[None, None, R]]) -> Callable[P, Coroutine[None, None, Either[Err, R]]]: # type: ignore
    ...
  @overload
  def __call__(self, fn: Callable[P, Awaitable[R]]) -> Callable[P, Awaitable[Either[Err, R]]]: # type: ignore
    ...
  @overload
  def __call__(self, fn: Callable[P, R]) -> Callable[P, Either[Err, R]]:
    ...

  def __call__(self, fn): # type: ignore
    if iscoroutinefunction(fn):
      @wraps(fn)
      async def _wrapper(*args: P.args, **kwargs: P.kwargs):
        try:
          return Right(await fn(*args, **kwargs))
        except Exception as e:
          if isinstance(e, self.exc):
            return Left(e)
          raise e
      return _wrapper
    else:
      @wraps(fn)
      def wrapper(*args: P.args, **kwargs: P.kwargs):
        try:
          return Right(fn(*args, **kwargs))
        except Exception as e:
          if isinstance(e, self.exc):
            return Left(e)
          raise e
      return wrapper

filter(eithers)

Source code in haskellian/src/haskellian/either/funcs.py
def filter(eithers: Iterable[Either[L, R]]) -> Iterable[R]:
  """"""
  for e in eithers:
    match e:
      case Right(value):
        yield value

filter_lefts(eithers)

Source code in haskellian/src/haskellian/either/funcs.py
def filter_lefts(eithers: Iterable[Either[L, R]]) -> Iterable[L]:
  """"""
  for e in eithers:
    match e:
      case Left(err):
        yield err

maybe(x)

Converts a nullable value to Either

Source code in haskellian/src/haskellian/either/funcs.py
def maybe(x: R | None) -> 'Either[None, R]':
  """Converts a nullable value to `Either`"""
  return Left(None) if x is None else Right(x)

sequence(eithers)

List of lefts if any, otherwise list of all rights.

E.sequence([Left(1), Right(2), Right(3), Left(4)]) # Left([1, 4])
E.sequence([Right(2), Right(3)]) # Right([2, 3])
Source code in haskellian/src/haskellian/either/funcs.py
def sequence(eithers: Iterable[Either[L, R]]) -> Either[list[L], list[R]]:
  """List of lefts if any, otherwise list of all rights.

  ```python
  E.sequence([Left(1), Right(2), Right(3), Left(4)]) # Left([1, 4])
  E.sequence([Right(2), Right(3)]) # Right([2, 3])
  ```
  """
  lefts: list[L] = []
  rights: list[R] = []
  for x in eithers:
    x.match(lefts.append, rights.append)
  return Right(rights) if lefts == [] else Left(lefts)

take_while(eithers)

Source code in haskellian/src/haskellian/either/funcs.py
def take_while(eithers: Iterable[Either[L, R]]) -> Iterable[R]:
  """"""
  for e in eithers:
    match e:
      case Right(x):
        yield x
      case _:
        return

Next up, asyn_iter