Skip to content

Latest commit

 

History

History
539 lines (378 loc) · 16 KB

File metadata and controls

539 lines (378 loc) · 16 KB

Container: the concept

.. currentmodule:: returns.primitives.container

Container is a concept that allows you to write code around the existing wrapped values while maintaining the execution context.

List of supported containers:

There are also some combinations like :class:`IOResult <returns.io.IOResult>`, :class:`FutureResult <returns.future.FutureResult>`, :class:`RequiresContextResult <.RequiresContextResult>`, :class:`RequiresContextIOResult <.RequiresContextIOResult>` and :class:`RequiresContextFutureResult <.RequiresContextFutureResult>`.

We will show you container's simple API of one attribute and several simple methods.

Basics

The main idea behind a container is that it wraps some internal state. That's what :attr:`._inner_value <returns.primitives.container.Container._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.

.. mermaid::
  :caption: State evolution.

  graph LR
    F1["Container(Initial)"] --> F2["Container(UserId(1))"]
    F2                       --> F3["Container(UserAccount(156))"]
    F3                       --> F4["Container(FailedLoginAttempt(1))"]
    F4                       --> F5["Container(SentNotificationId(992))"]


Working with a container

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 :func:`returns.interfaces.mappable.MappableN.map` to compose containers with regular functions.

Here's how it looks:

.. mermaid::
  :caption: Illustration of ``map`` method.

  graph LR
    F1["Container[A]"] -- "map(function)" --> F2["Container[B]"]

    style F1 fill:green
    style F2 fill:green

>>> 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. :func:`returns.interfaces.bindable.BindableN.bind` is used to literally bind two different containers together.

Here's how it looks:

.. mermaid::
  :caption: Illustration of ``bind`` method.

  graph LR
    F1["Container[A]"] -- "bind(function)" --> F2["Container[B]"]
    F1["Container[A]"] -- "bind(function)" --> F3["Container[C]"]

    style F1 fill:green
    style F2 fill:green
    style F3 fill:red

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 :class:`returns.interfaces.mappable.MappableN` and :class:`returns.interfaces.bindable.BindableN`.

You can read more about methods that some other containers support and :ref:`interfaces <base-interfaces>` behind them.

Instantiating a container

All :class:`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:

>>> from returns.maybe import Maybe, Some, Nothing
>>> assert Maybe.from_optional(1) == Some(1)
>>> assert Maybe.from_optional(None) == Nothing
>>> 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.

Working with multiple containers

Multiple container arguments

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:

  1. We make sum_two_numbers to receive :ref:`partial arguments <curry>`
  2. We create a new container that wraps sum_two_numbers function as a value
  3. 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).

Working with iterable of containers

Imagine that you have to take 10 random numbers and then sum them 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 :meth:`Fold.loop <returns.iterables.AbstractFold.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.

Collecting an iterable of containers into a single container

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 :meth:`Fold.collect <returns.iterables.AbstractFold.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 :meth:`Fold.collect_all <returns.iterables.AbstractFold.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.

Immutability

We like to think of returns as :ref:`immutable <primitive-types>` 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 :class:`returns.primitives.types.Immutable` mixin that users can use to quickly make their classes immutable.

Creating Modified Copies of Containers

While containers are immutable, sometimes you need to create a modified copy of a container with different inner values. Since Python 3.13, returns containers support the copy.replace() function via the __replace__ magic method.

>>> from returns.result import Success, Failure
>>> import copy, sys
>>>
>>> # Only run this example on Python 3.13+
>>> if sys.version_info >= (3, 13):
...     # Replace the inner value of a Success container
...     original = Success(1)
...     modified = copy.replace(original, _inner_value=2)
...     assert modified == Success(2)
...     assert original is not modified  # Creates a new instance
...
...     # Works with Failure too
...     error = Failure("original error")
...     new_error = copy.replace(error, _inner_value="new error message")
...     assert new_error == Failure("new error message")
...
...     # No changes returns the original object (due to immutability)
...     assert copy.replace(original) is original
... else:
...     # For Python versions before 3.13, the tests would be skipped
...     pass

Note

The parameter name _inner_value is used because it directly maps to the internal attribute of the same name in BaseContainer. In the __replace__ implementation, this parameter name is specifically recognized to create a new container instance with a modified inner value.

Warning

While copy.replace() works at runtime, it has limitations with static type checking. If you replace an inner value with a value of a different type, type checkers won't automatically infer the new type:

# Example that would work in Python 3.13+:
# >>> num_container = Success(123)
# >>> str_container = copy.replace(num_container, _inner_value="string")
# >>> # Type checkers may still think this is Success[int] not Success[str]
>>> # The above is skipped in doctest as copy.replace requires Python 3.13+

Using copy.replace() with Custom Containers

If you create your own container by extending BaseContainer, it will automatically inherit the __replace__ implementation for free. This means your custom containers will work with copy.replace() just like the built-in ones.

>>> from returns.primitives.container import BaseContainer
>>> from typing import TypeVar, Generic
>>> import copy, sys  # Requires Python 3.13+ for copy.replace

>>> T = TypeVar('T')
>>> class MyBox(BaseContainer, Generic[T]):
...     """A custom container that wraps a value."""
...     def __init__(self, inner_value: T) -> None:
...         super().__init__(inner_value)
...
...     def __eq__(self, other: object) -> bool:
...         if not isinstance(other, MyBox):
...             return False
...         return self._inner_value == other._inner_value
...
...     def __repr__(self) -> str:
...         return f"MyBox({self._inner_value!r})"

>>> # Create a basic container
>>> box = MyBox("hello")
>>>
>>> # Test works with copy.replace only on Python 3.13+
>>> if sys.version_info >= (3, 13):
...     new_box = copy.replace(box, _inner_value="world")
...     assert new_box == MyBox("world")
...     assert box is not new_box
... else:
...     # For Python versions before 3.13
...     pass

By inheriting from BaseContainer, your custom container will automatically support:

  1. The basic container operations like __eq__, __hash__, __repr__
  2. Pickling via __getstate__ and __setstate__
  3. The copy.replace() functionality via __replace__
  4. Immutability via the Immutable mixin

Before Python 3.13, you can use container-specific methods to create modified copies:

>>> from returns.result import Success, Failure, Result
>>> from typing import Any

>>> # For Success containers, we can use .map to transform the inner value
>>> original = Success(1)
>>> modified = original.map(lambda _: 2)
>>> assert modified == Success(2)

>>> # For Failure containers, we can use .alt to transform the inner value
>>> error = Failure("error")
>>> new_error = error.alt(lambda _: "new error")
>>> assert new_error == Failure("new error")

>>> # For general containers without knowing success/failure state:
>>> def replace_inner_value(container: Result[Any, Any], new_value: Any) -> Result[Any, Any]:
...     """Create a new container with the same state but different inner value."""
...     if container.is_success():
...         return Success(new_value)
...     return Failure(new_value)

Type safety

We try to make our containers optionally type safe.

What does it mean?

  1. It is still good old python, do whatever you want without mypy
  2. 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 :ref:`mypy plugins <mypy-plugins>`.

Further reading

API Reference

BaseContainer is a base class for all other containers. It defines some basic things like representation, hashing, pickling, etc.

.. autoclasstree:: returns.primitives.container
   :strict:

.. automodule:: returns.primitives.container
   :members:
   :special-members: