Containers can serve many different purposes
(while still serving the main one: composition)
for example, some of them
(Result
and Maybe
) are used
to work with different types of errors
starting with NullPointerException
to arbitrary user-defined ones.
When talking about error handling we use a concept of Railway oriented programming. It means that flow of our program has two tracks:
Successful one: where everything goes perfectly: HTTP requests work, database is always serving us data, parsing values does not fail
Failed one: where something went wrong
We can switch from track to track: we can fail something or we can fix the situation.
Railway oriented programming.¶
We also support two special methods to work with “failed” values:
returns.interfaces.altable.AltableN.alt()
transforms error to another error
that works only when container is in failed state,
is the opposite of returns.interfaces.mappable.MappableN.map()
method
returns.interfaces.lashable.LashableN.lash()
is the opposite of returns.interfaces.bindable.BindableN.bind()
method
that works only when container is in failed state
Let’s start from the first one:
alt
method allows to change your error type.
Illustration of alt
method.¶
>>> from returns.result import Failure
>>> assert Failure(1).alt(str) == Failure('1')
The second method is lash
. It is a bit different.
We pass a function that returns another container to it.
returns.interfaces.lashable.LashableN.lash()
is used to literally bind two different containers together.
It can also lash your flow and get on the successful track again:
Illustration of lash
method.¶
>>> from returns.result import Result, Failure, Success
>>> def tolerate_exception(state: Exception) -> Result[int, Exception]:
... if isinstance(state, ZeroDivisionError):
... return Success(0)
... return Failure(state)
>>> value: Result[int, Exception] = Failure(ZeroDivisionError())
>>> result: Result[int, Exception] = value.lash(tolerate_exception)
>>> assert result == Success(0)
>>> value2: Result[int, Exception] = Failure(ValueError())
>>> result2: Result[int, Exception] = value2.lash(tolerate_exception)
>>> # => Failure(ValueError())
From typing perspective .alt
and .lash
are exactly the same as .map
and .bind
but only work with the second type argument instead of the first one:
from returns.result import Result
first: Result[int, int]
second: Result[int, int]
reveal_type(first.map(str))
# => Result[str, int]
reveal_type(second.alt(str))
# => Result[int, str]
Note
Not all containers support these methods,
only containers that implement
returns.interfaces.lashable.LashableN
and
returns.interfaces.altable.AltableN
For example, IO
based containers
and RequiresContext
cannot be alted or lashed.
And we have two more functions to unwrap inner state of containers into a regular types:
.unwrap
returns a value if it is possible,
raises returns.primitives.exceptions.UnwrapFailedError
otherwise
>>> from returns.result import Failure, Success
>>> from returns.maybe import Some, Nothing
>>> assert Success(1).value_or(None) == 1
>>> assert Some(0).unwrap() == 0
>>> Failure(1).unwrap()
Traceback (most recent call last):
...
returns.primitives.exceptions.UnwrapFailedError
>>> Nothing.unwrap()
Traceback (most recent call last):
...
returns.primitives.exceptions.UnwrapFailedError
For failing containers you can
use returns.interfaces.unwrappable.Unwrapable.failure()
to unwrap the failed state:
>>> assert Failure(1).failure() == 1
>>> Success(1).failure()
Traceback (most recent call last):
...
returns.primitives.exceptions.UnwrapFailedError
Be careful, since this method will raise an exception
when you try to .failure()
a successful container.
Note
Not all containers support these methods,
only containers that implement
returns.interfaces.unwrappable.Unwrappable
.
For example, IO
based containers
and RequiresContext
cannot be unwrapped.
Note
Some containers also have .value_or()
helper method.
Example:
>>> from returns.result import Success, Failure
>>> assert Success(1).value_or(None) == 1
>>> assert Failure(1).value_or(None) is None