.. _railway: Railway oriented programming ============================ Containers can serve many different purposes (while still serving the main one: composition) for example, some of them (:class:`~returns.result.Result` and :class:`~returns.maybe.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: 1. Successful one: where everything goes perfectly: HTTP requests work, database is always serving us data, parsing values does not fail 2. Failed one: where something went wrong We can switch from track to track: we can fail something or we can fix the situation. .. mermaid:: :caption: Railway oriented programming. 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 Returning execution to the right track ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ We also support two special methods to work with "failed" values: - :func:`returns.interfaces.altable.AltableN.alt` transforms error to another error that works only when container is in failed state, is the opposite of :func:`returns.interfaces.mappable.MappableN.map` method - :func:`returns.interfaces.lashable.LashableN.lash` is the opposite of :func:`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. .. mermaid:: :caption: Illustration of ``alt`` method. graph LR F1["Container[A]"] -- "alt(function)" --> F2["Container[B]"] style F1 fill:red style F2 fill:red .. code:: python >>> 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. :func:`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: .. mermaid:: :caption: Illustration of ``lash`` method. 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 .. code:: python >>> 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: .. code:: python 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 :class:`returns.interfaces.lashable.LashableN` and :class:`returns.interfaces.altable.AltableN` For example, :class:`~returns.io.IO` based containers and :class:`~returns.context.requires_context.RequiresContext` cannot be alted or lashed. Unwrapping values ----------------- And we have two more functions to unwrap inner state of containers into a regular types: - :func:`.unwrap ` returns a value if it is possible, raises :class:`returns.primitives.exceptions.UnwrapFailedError` otherwise .. code:: python >>> from returns.result import Failure, Success >>> from returns.maybe import Some, Nothing >>> assert Success(1).value_or(None) == 1 >>> assert Some(0).unwrap() == 0 .. code:: pycon >>> 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 :meth:`returns.interfaces.unwrappable.Unwrapable.failure` to unwrap the failed state: .. code:: pycon >>> 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 :class:`returns.interfaces.unwrappable.Unwrappable`. For example, :class:`~returns.io.IO` based containers and :class:`~returns.context.requires_context.RequiresContext` cannot be unwrapped. .. note:: Some containers also have ``.value_or()`` helper method. Example: .. code:: python >>> from returns.result import Success, Failure >>> assert Success(1).value_or(None) == 1 >>> assert Failure(1).value_or(None) is None Further reading --------------- - `Railway oriented programming in F# `_ - `Against Railway-Oriented Programming `_