Container is a concept that allows you to write code around the existing wrapped values while maintaining the execution context.
List of supported containers:
Maybe
to handle None
cases
Result
to handle possible exceptions
IO
to mark explicit IO
actions
Future
to work with async
code
RequiresContext
to pass context to your functions (DI and similar)
There are also some combintations like
IOResult
,
FutureResult
,
RequiresContextResult
and
RequiresContextIOResult
.
We will show you container’s simple API of one attribute and several simple methods.
The main idea behind a container is that it wraps some internal state.
That’s what
._inner_value
is used for.
And we have several functions to create new containers based on the previous state. And we can see how this state is evolving during the execution.
All containers support special .from_value
method
to construct a new container from a raw value.
>>> from returns.result import Result
>>> str(Result.from_value(1))
'<Success: 1>'
We use two methods to create a new container from the previous one.
bind
and map
.
The difference is simple:
map
works with functions that return regular value
bind
works with functions that return new container of the same type
bind()
is used to literally bind two different containers together.
from returns.result import Result, Success
def may_fail(user_id: int) -> Result[float, str]:
...
value: Result[int, str] = Success(1)
# Can be assumed as either Success[float] or Failure[str]:
result: Result[float, str] = value.bind(may_fail)
And we have map()
to use containers with regular functions.
>>> from typing import Any
>>> from returns.result import Success, Result
>>> def double(state: int) -> int:
... return state * 2
>>> result: Result[int, Any] = Success(1).map(double)
>>> str(result)
'<Success: 2>'
>>> result: Result[int, Any] = result.map(lambda state: state + 1)
>>> str(result)
'<Success: 3>'
The same works with built-in functions as well:
>>> from returns.io import IO
>>> io = IO('bytes').map(list)
>>> str(io)
"<IO: ['b', 'y', 't', 'e', 's']>"
Note:
All containers support these methods.
You can read more about methods that some other containers support and interfaces behind them.
You might end up with an iterable of containers:
>>> from returns.maybe import Maybe, Some, Nothing, maybe
>>> source = {'a': 1, 'b': 2}
>>> fetched_values: Maybe[int] = [
... maybe(source.get)(key)
... for key in ('a', 'b')
... ]
To work with iterable of containers, it is recommended to cast it into a container with the iterable inside:
>>> assert Maybe.from_iterable(fetched_values) == Some((1, 2))
Any falsy values will result in a falsy result (pun intended):
>>> fetched_values: Maybe[int] = [
... maybe(source.get)(key)
... for key in ('a', 'c') # 'c' is missing!
... ]
>>> assert Maybe.from_iterable(fetched_values) == Nothing
We support any Iterable[T]
input type
and return a Container[Sequence[T]]
.
All containers support this method.
We have already seen how we can work with one container and functions that receive a single argument.
Let’s say you have a function of two arguments and two containers:
def sum_two_numbers(first: int, second: int) -> int:
return first + second
And here are our two containers:
from returns.io import IO
one = IO(1)
two = IO(2)
Naive approach to compose two IO
containers and a function
would be two hard to show here.
Luckly, we support partial application and .apply()
method.
Here are the required steps:
We make sum_two_numbers
to receive partial arguments
We create a new container that wraps sum_two_numbers
function as a value
We then call .apply()
twice to pass each value
It can be done like so:
>>> from returns.curry import curry
>>> from returns.io import IO
>>> @curry
... def sum_two_numbers(first: int, second: int) -> int:
... return first + second
>>> one = IO(1)
>>> two = IO(2)
>>> assert two.apply(one.apply(IO(sum_two_numbers))) == IO(3)
But, there are other ways to make sum_two_numbers
partial. One can use:
>>> one = IO(1)
>>> two = IO(2)
>>> assert two.apply(one.apply(
... IO(lambda x: lambda y: sum_two_numbers(x, y)),
... )) == IO(3)
It would be faster, but not as elegant (and type-safe).
We like to think of returns
as immutable structures.
You cannot mutate the inner state of the created container,
because we redefine __setattr__
and __delattr__
magic methods.
You cannot also set new attributes to container instances,
since we are using __slots__
for better performance and strictness.
Well, nothing is really immutable in python, but you were warned.
We also provide returns.primitives.types.Immutable
mixin
that users can use to quickly make their classes immutable.
We try to make our containers optionally type safe.
What does it mean?
It is still good old python
, do whatever you want without mypy
If you are using mypy
you will be notified about type violations
We also ship PEP561
compatible .pyi
files together with the source code.
In this case these types will be available to users
when they install our application.
We also ship custom mypy
plugins to overcome some existing problems,
please make sure to use them,
since they increase your developer experience and type-safety level:
Check out our docs on using our mypy plugins.
You can and should compose different containers together. Here’s a table of some compositions that do not make sense:
You can use Converters to convert Maybe
and Result
containers.
You can also use flatten
to merge nested containers.
IO[Result[A, B]]
🤔,
use returns.io.IOResult.from_typecast()
and IOResult
IO[Maybe[A]]
🤔,
use maybe_to_result
and then returns.io.IOResult.from_typecast()
to convert it to IOResult
IO[IO[A]]
🤔, use flatten
Maybe[Maybe[A]]
🤔, use flatten
Result[Result[A, B], C]
🤔,
use flatten
Result[Maybe[A], B]
🤔,
use maybe_to_result
and then flatten
Maybe[Result[A, B]]
🤔,
use result_to_maybe
and then flatten
RequiresContext[env, Result[A, B]]
🤔,
use RequiresContextResult.from_typecast
and RequiresResultContext
RequiresContext[env, RequiresContext[env, A]]
🤔,
use flatten
RequiresContextResult[env, RequiresContextResult[env, A, B], B]
🤔,
use flatten
RequiresContext[env, IOResult[A, B]]
🤔,
use RequiresContextIOResult.from_typecast
and RequiresResultContext
RequiresContextIOResult[env, RequiresContextIOResult[env, A, B], B]
🤔,
use flatten
Result[IO[A], B]
🚫
Result[A, IO[A]]
🚫
BaseContainer
is a base class for all other containers.
It defines some basic things like representation, hashing, pickling, etc.
BaseContainer
(inner_value)[source]¶Bases: returns.primitives.types.Immutable
Utility class to provide all needed magic methods to the context.
__init__
(inner_value)[source]¶Wraps the given value in the Container.
‘value’ is any arbitrary value of any type including functions.
None
Here are our interfaces (or protocols to be more specific) that we use inside our app:
Bindable
(*args, **kwargs)[source]¶Bases: typing_extensions.Protocol
Represents a “context” in which calculations can be executed.
Bindable
allows you to bind together
a series of calculations while maintaining
the context of that specific container.
Unifiable
(*args, **kwargs)[source]¶Bases: typing_extensions.Protocol
Represents a “context” in which calculations can be executed.
Unifiable
allows you to bind together
a series of calculations while maintaining
the context of that specific container.
As the name suggests is used to unify error types while binding the value type.
Mappable
(*args, **kwargs)[source]¶Bases: typing_extensions.Protocol
Allows to chain wrapped values with regular functions.
Behaves like a functor.
Fixable
(*args, **kwargs)[source]¶Bases: typing_extensions.Protocol
Represents containers that can be fixed and rescued.
fix
(function)[source]¶Applies ‘function’ to the error and transforms failure to success.
And returns a new functor value.
Works for containers that represent failure.
Is the opposite of map()
.
function (Callable
[[+_ErrorType], ~_NewValueType]) –
Fixable
[~_NewValueType, +_ErrorType]
Rescueable
(*args, **kwargs)[source]¶Bases: typing_extensions.Protocol
Represents a “context” in which calculations can be executed.
Rescueable
allows you to bind together
a series of calculations while maintaining
the context of that specific container.
rescue
(function)[source]¶Applies ‘function’ to the result of a previous calculation.
And returns a new container.
Works for containers that represent failure.
Is the opposite of bind()
.
function (Callable
[[+_ErrorType], Rescueable
[~_NewValueType, ~_NewErrorType]]) –
Rescueable
[~_NewValueType, ~_NewErrorType]
Unwrapable
(*args, **kwargs)[source]¶Bases: typing_extensions.Protocol
Represents containers that can unwrap and return its wrapped value.
value_or
(default_value)[source]¶Forces to unwrap value from container or return a default.
default_value (~_NewValueType) –
Union
[+_ValueType, ~_NewValueType]
Altable
(*args, **kwargs)[source]¶Bases: typing_extensions.Protocol
Allows to unwrap failures.
Applicative
(*args, **kwargs)[source]¶Bases: typing_extensions.Protocol
Allows to create unit containers from raw values.
All containers should support this interface.
See also
https://en.wikipedia.org/wiki/Applicative_functor http://learnyouahaskell.com/functors-applicative-functors-and-monoids
from_value
(inner_value)[source]¶This method is required to create new containers.
inner_value (~_NewValueType) –
Unitable
[~_NewValueType, Any
]
apply
(container)[source]¶Calls a wrapped function in a container on this container.
container (Applicative
[Callable
[[+_ValueType], ~_NewValueType]]) –
Applicative
[~_NewValueType]
Unitable
(*args, **kwargs)[source]¶Bases: returns.primitives.interfaces.Applicative
, typing_extensions.Protocol
Allows to create unit values from success and failure.
This is a heavily Result
-related class.