Railway oriented programming¶
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.
Error handling¶
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.
graph LR
S1 -- bind --> S3
S1 -- bind --> F2
S3 -- map --> S5
S5 -- bind --> S7
S5 -- bind --> F6
F2 -- alt --> F4
F4 -- lash --> F6
F4 -- lash --> S5
F6 -- lash --> F8
F6 -- lash --> S7
style S1 fill:green
style S3 fill:green
style S5 fill:green
style S7 fill:green
style F2 fill:red
style F4 fill:red
style F6 fill:red
style F8 fill:red
Railway oriented programming.¶
Returning execution to the right track¶
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 ofreturns.interfaces.mappable.MappableN.map()methodreturns.interfaces.lashable.LashableN.lash()is the opposite ofreturns.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.
graph LR
F1["Container[A]"] -- "alt(function)" --> F2["Container[B]"]
style F1 fill:red
style F2 fill:red
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:
graph LR
F1["Container[A]"] -- "lash(function)" --> F2["Container[B]"]
F1["Container[A]"] -- "lash(function)" --> F3["Container[C]"]
style F1 fill:red
style F2 fill:green
style F3 fill:red
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.
Unwrapping values¶
And we have two more functions to unwrap inner state of containers into a regular types:
.unwrapreturns a value if it is possible, raisesreturns.primitives.exceptions.UnwrapFailedErrorotherwise
>>> 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