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