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
,
RequiresContextIOResult
and
RequiresContextFutureResult
.
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.
State evolution.¶
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
We have returns.interfaces.mappable.MappableN.map()
to compose containers with regular functions.
Here’s how it looks:
Illustration of map
method.¶
>>> 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)
>>> assert str(result) == '<Success: 2>'
>>> result: Result[int, Any] = result.map(lambda state: state + 1)
>>> assert 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']>"
The second method is bind
. It is a bit different.
We pass a function that returns another container to it.
returns.interfaces.bindable.BindableN.bind()
is used to literally bind two different containers together.
Here’s how it looks:
Illustration of bind
method.¶
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)
Note
All containers support these methods.
Because all containers implement
returns.interfaces.mappable.MappableN
and
returns.interfaces.bindable.BindableN
.
You can read more about methods that some other containers support and interfaces behind them.
All returns.interfaces.applicative.ApplicativeN
containers
support special .from_value
method
to construct a new container from a raw value.
>>> from returns.result import Result
>>> assert str(Result.from_value(1)) == '<Success: 1>'
There are also other methods in other interfaces. For example, here are some of them:
returns.interfaces.specific.maybe.MaybeLikeN.from_optional()
creates a value from Optional
value
>>> from returns.maybe import Maybe, Some, Nothing
>>> assert Maybe.from_optional(1) == Some(1)
>>> assert Maybe.from_optional(None) == Nothing
returns.interfaces.failable.DiverseFailableN.from_failure()
creates a failing container from a value
>>> from returns.result import Result, Failure
>>> assert Result.from_failure(1) == Failure(1)
There are many other constructors! Check out concrete types and their interfaces.
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)
The naive approach to compose two IO
containers and a function
would be too hard to show here.
Luckily, we support partial application and the .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 partial
as well:
>>> from returns.curry import partial
>>> one = IO(1)
>>> two = IO(2)
>>> assert two.apply(one.apply(
... IO(lambda x: partial(sum_two_numbers, x)),
... )) == IO(3)
Or even native lambda
functions:
>>> 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).
Imagine that you have two take 10 random numbers and then sum they to get the final result.
So, here’s how your code will look like:
>>> import random
>>> from returns.io import IO
>>> def random_number() -> IO[int]:
... return IO(2) # Example, basically alias of ``random.randint(1, 5)``
>>> numbers = [random_number() for _ in range(10)]
>>> assert len(numbers) == 10
>>> assert all(isinstance(number, IO) for number in numbers)
So, how to sum these random values into a single IO[int]
value?
That’s where
Fold.loop
really helps!
>>> from typing import Callable
>>> from returns.iterables import Fold
>>> def sum_two_numbers(first: int) -> Callable[[int], int]:
... return lambda second: first + second
>>> assert Fold.loop(
... numbers, # let's loop on our ``IO`` values
... IO(0), # starting from ``0`` value
... sum_two_numbers, # and getting the sum of each two numbers in a loop
... ) == IO(20)
We can also change the initial element to some other value:
>>> assert Fold.loop(
... numbers,
... IO(5), # now we will start from ``5``, not ``0`
... sum_two_numbers,
... ) == IO(25)
Fold.loop
is eager. It will be executed for all items in your iterable.
You might end up with an iterable of containers:
>>> from typing import List
>>> from returns.maybe import Maybe, Some, Nothing, maybe
>>> source = {'a': 1, 'b': 2}
>>> fetched_values: List[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
using the Fold.collect
method:
>>> from returns.iterables import Fold
>>> assert Fold.collect(fetched_values, Some(())) == Some((1, 2))
Any falsy values will result in a falsy result (pun intended):
>>> fetched_values: List[Maybe[int]] = [
... maybe(source.get)(key)
... for key in ('a', 'c') # 'c' is missing!
... ]
>>> assert Fold.collect(fetched_values, Some(())) == Nothing
You can also use a different strategy to fetch values you need,
to do just that we have
Fold.collect_all
method:
>>> fetched_values: Maybe[int] = [
... maybe(source.get)(key)
... for key in ('a', 'c') # 'c' is missing!
... ]
>>> assert Fold.collect_all(fetched_values, Some(())) == Some((1,))
We support any Iterable[T]
input type
and return a Container[Sequence[T]]
.
You can subclass Fold
type to change how any of these methods work.
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.
BaseContainer
is a base class for all other containers.
It defines some basic things like representation, hashing, pickling, etc.
Bases: Immutable
Utility class to provide all needed magic methods to the context.
Wraps the given value in the Container.
‘value’ is any arbitrary value of any type including functions.
Used to compare two ‘Container’ objects.
other (Any
) –
bool
Loading state from pickled data.
state (Union
[_PickleState
, Any
]) –
None