Create your own container

This tutorial will guide you through the process of creating your own containers.

Step 0: Motivation

First things first, why would anyone want to create a custom containers?

The great idea about “containers” in functional programming is that it can be literally anything. There are endless use-cases.

You can create your own primitives for working with some language-or-framework specific problem, or just model your business domain.

You can copy ideas from other languages or just compose existing containers for better usability (like IOResult is the composition of IO and Result).

Example

We are going to implement a Pair container for this example. What is a Pair? Well, it is literally a pair of two values. No more, no less. Similar to a Tuple[FirstType, SecondType]. But with extra goodies.

Note

You can find all code samples here.

Step 1: Choosing right interfaces

After you came up with the idea, you will need to make a decision: what capabilities my container must have?

Basically, you should decide what Interfaces you will subtype and what methods and laws will be present in your type. You can create just a returns.interfaces.mappable.MappableN or choose a full featured returns.interfaces.container.ContainerN.

You can also choose some specific interfaces to use, like returns.interfaces.specific.result.ResultLikeN or any other.

Summing up, decide what laws and methods you need to solve your problem. And then subtype the interfaces that provide these methods and laws.

Example

What interfaces a Pair type needs?

Now, after we know about all interfaces we would need, let’s find pre-defined aliases we can reuse.

Turns out, there are some of them!

Let’s look at the result:

Note

A special note on returns.primitives.container.BaseContainer. It is a very useful class with lots of pre-defined feaatures, like: immutability, better cloning, serialization, and comparison.

You can skip it if you wish, but it is highlighly recommended.

Later we will talk about an actual implementation of all required methods.

Step 2: Initial implementation

So, let’s start writing some code!

We would need to implement all interface methods, otherwise mypy won’t be happy. That’s what it currently says on our type definition:

error: Final class test_pair1.Pair has abstract attributes "alt", "bind", "equals", "lash", "map", "swap"

Looks like it already knows what methods should be there!

Ok, let’s drop some initial and straight forward implementation. We will later make it more complex step by step.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
from typing import Callable, Tuple, TypeVar

from typing_extensions import final

from returns.interfaces import bindable, equable, lashable, swappable
from returns.primitives.container import BaseContainer, container_equality
from returns.primitives.hkt import Kind2, SupportsKind2, dekind

_FirstType = TypeVar('_FirstType')
_SecondType = TypeVar('_SecondType')

_NewFirstType = TypeVar('_NewFirstType')
_NewSecondType = TypeVar('_NewSecondType')


@final
class Pair(
    BaseContainer,
    SupportsKind2['Pair', _FirstType, _SecondType],
    bindable.Bindable2[_FirstType, _SecondType],
    swappable.Swappable2[_FirstType, _SecondType],
    lashable.Lashable2[_FirstType, _SecondType],
    equable.Equable,
):
    """
    A type that represents a pair of something.

    Like to coordinates ``(x, y)`` or two best friends.
    Or a question and an answer.

    """

    def __init__(
        self,
        inner_value: Tuple[_FirstType, _SecondType],
    ) -> None:
        """Saves passed tuple as ``._inner_value`` inside this instance."""
        super().__init__(inner_value)

    # `Equable` part:

    equals = container_equality  # we already have this defined for all types

    # `Mappable` part via `BiMappable`:

    def map(  # noqa: WPS125
        self,
        function: Callable[[_FirstType], _NewFirstType],
    ) -> 'Pair[_NewFirstType, _SecondType]':
        return Pair((function(self._inner_value[0]), self._inner_value[1]))

    # `BindableN` part:

    def bind(
        self,
        function: Callable[
            [_FirstType],
            Kind2['Pair', _NewFirstType, _SecondType],
        ],
    ) -> 'Pair[_NewFirstType, _SecondType]':
        return dekind(function(self._inner_value[0]))

    # `AltableN` part via `BiMappableN`:

    def alt(
        self,
        function: Callable[[_SecondType], _NewSecondType],
    ) -> 'Pair[_FirstType, _NewSecondType]':
        return Pair((self._inner_value[0], function(self._inner_value[1])))

    # `LashableN` part:

    def lash(
        self,
        function: Callable[
            [_SecondType],
            Kind2['Pair', _FirstType, _NewSecondType],
        ],
    ) -> 'Pair[_FirstType, _NewSecondType]':
        return dekind(function(self._inner_value[1]))

    # `SwappableN` part:

    def swap(self) -> 'Pair[_SecondType, _FirstType]':
        return Pair((self._inner_value[1], self._inner_value[0]))

You can check our resulting source with mypy. It would be happy this time.

Step 3: New interfaces

As you can see our existing interfaces do not cover everything. We can potentially want several extra things:

  1. A method that takes two arguments and returns a new Pair instance

  2. A named constructor to create a Pair from a single value

  3. A named constructor to create a Pair from two values

We can define an interface just for this! It would be also nice to add all other interfaces there as supertypes.

That’s how it is going to look:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
class PairLikeN(
    bindable.BindableN[_FirstType, _SecondType, _ThirdType],
    swappable.SwappableN[_FirstType, _SecondType, _ThirdType],
    lashable.LashableN[_FirstType, _SecondType, _ThirdType],
    equable.Equable,
):
    """Special interface for types that look like a ``Pair``."""

    @abstractmethod
    def pair(
        self: _PairLikeKind,
        function: Callable[
            [_FirstType, _SecondType],
            KindN[_PairLikeKind, _NewFirstType, _NewSecondType, _ThirdType],
        ],
    ) -> KindN[_PairLikeKind, _NewFirstType, _NewSecondType, _ThirdType]:
        """Allows to work with both arguments at the same time."""

    @classmethod
    @abstractmethod
    def from_paired(
        cls: Type[_PairLikeKind],
        first: _NewFirstType,
        second: _NewSecondType,
    ) -> KindN[_PairLikeKind, _NewFirstType, _NewSecondType, _ThirdType]:
        """Allows to create a PairLikeN from just two values."""

    @classmethod
    @abstractmethod
    def from_unpaired(
        cls: Type[_PairLikeKind],
        inner_value: _NewFirstType,
    ) -> KindN[_PairLikeKind, _NewFirstType, _NewFirstType, _ThirdType]:
        """Allows to create a PairLikeN from just a single object."""

Awesome! Now we have a new interface to implement. Let’s do that!

1
2
3
4
5
6
7
8
    def pair(
        self,
        function: Callable[
            [_FirstType, _SecondType],
            Kind2['Pair', _NewFirstType, _NewSecondType],
        ],
    ) -> 'Pair[_NewFirstType, _NewSecondType]':
        return dekind(function(self._inner_value[0], self._inner_value[1]))
1
2
3
4
5
6
    @classmethod
    def from_unpaired(
        cls,
        inner_value: _NewFirstType,
    ) -> 'Pair[_NewFirstType, _NewFirstType]':
        return Pair((inner_value, inner_value))

Looks like we are done!

Step 4: Writing tests and docs

The best part about this type is that it is pure. So, we can write our tests inside docs!

We are going to use doctests builtin module for that.

This gives us several key benefits:

  • All our docs has usage examples

  • All our examples are correct, because they are executed and tested

  • We don’t need to write regular boring tests

Let’s add docs and doctests! Let’s use map method as a short example:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
    def map(  # noqa: WPS125
        self,
        function: Callable[[_FirstType], _NewFirstType],
    ) -> 'Pair[_NewFirstType, _SecondType]':
        """
        Changes the first type with a pure function.

        >>> assert Pair((1, 2)).map(str) == Pair(('1', 2))

        """
        return Pair((function(self._inner_value[0]), self._inner_value[1]))

By adding these simple tests we would already have 100% coverage. But, what if we can completely skip writing tests, but still have 100%?

Let’s discuss how we can achieve that with “Laws as values”.

Step 5: Checking laws

We already ship lots of laws with our interfaces. See our docs on laws and checking them.

Moreover, you can also define your own laws! Let’s add them to our PairLikeN interface.

Let’s start with laws definition:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
class _LawSpec(LawSpecDef):
    @law_definition
    def pair_equality_law(
        raw_value: _FirstType,
        container: 'PairLikeN[_FirstType, _SecondType, _ThirdType]',
    ) -> None:
        """Ensures that unpaired and paired constructors work fine."""
        assert_equal(
            container.from_unpaired(raw_value),
            container.from_paired(raw_value, raw_value),
        )

    @law_definition
    def pair_left_identity_law(
        pair: Tuple[_FirstType, _SecondType],
        container: 'PairLikeN[_FirstType, _SecondType, _ThirdType]',
        function: Callable[
            [_FirstType, _SecondType],
            KindN['PairLikeN', _NewFirstType, _NewSecondType, _ThirdType],
        ],
    ) -> None:
        """Ensures that unpaired and paired constructors work fine."""
        assert_equal(
            container.from_paired(*pair).pair(function),
            function(*pair),
        )

And them let’s add them to our PairLikeN interface:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
class PairLikeN(
    bindable.BindableN[_FirstType, _SecondType, _ThirdType],
    swappable.SwappableN[_FirstType, _SecondType, _ThirdType],
    lashable.LashableN[_FirstType, _SecondType, _ThirdType],
    equable.Equable,
):
    """Special interface for types that look like a ``Pair``."""

    _laws: ClassVar[Sequence[Law]] = (
        Law2(_LawSpec.pair_equality_law),
        Law3(_LawSpec.pair_left_identity_law),
    )

    @abstractmethod
    def pair(
        self: _PairLikeKind,
        function: Callable[
            [_FirstType, _SecondType],
            KindN[_PairLikeKind, _NewFirstType, _NewSecondType, _ThirdType],
        ],
    ) -> KindN[_PairLikeKind, _NewFirstType, _NewSecondType, _ThirdType]:
        """Allows to work with both arguments at the same time."""

    @classmethod
    @abstractmethod
    def from_paired(
        cls: Type[_PairLikeKind],
        first: _NewFirstType,
        second: _NewSecondType,
    ) -> KindN[_PairLikeKind, _NewFirstType, _NewSecondType, _ThirdType]:
        """Allows to create a PairLikeN from just two values."""

    @classmethod
    @abstractmethod
    def from_unpaired(
        cls: Type[_PairLikeKind],
        inner_value: _NewFirstType,
    ) -> KindN[_PairLikeKind, _NewFirstType, _NewFirstType, _ThirdType]:
        """Allows to create a PairLikeN from just a single object."""

The last to do is to call check_all_laws(Pair, use_init=True) to generate 10 hypothesis test cases with hundreds real test cases inside.

Here’s the final result of our brand new Pair type:

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
from abc import abstractmethod
from typing import Callable, ClassVar, NoReturn, Sequence, Tuple, Type, TypeVar

from typing_extensions import final

from returns.contrib.hypothesis.laws import check_all_laws
from returns.interfaces import bindable, equable, lashable, swappable
from returns.primitives.asserts import assert_equal
from returns.primitives.container import BaseContainer, container_equality
from returns.primitives.hkt import Kind2, KindN, SupportsKind2, dekind
from returns.primitives.laws import Law, Law2, Law3, LawSpecDef, law_definition

_FirstType = TypeVar('_FirstType')
_SecondType = TypeVar('_SecondType')
_ThirdType = TypeVar('_ThirdType')

_NewFirstType = TypeVar('_NewFirstType')
_NewSecondType = TypeVar('_NewSecondType')

_PairLikeKind = TypeVar('_PairLikeKind', bound='PairLikeN')


class _LawSpec(LawSpecDef):
    @law_definition
    def pair_equality_law(
        raw_value: _FirstType,
        container: 'PairLikeN[_FirstType, _SecondType, _ThirdType]',
    ) -> None:
        """Ensures that unpaired and paired constructors work fine."""
        assert_equal(
            container.from_unpaired(raw_value),
            container.from_paired(raw_value, raw_value),
        )

    @law_definition
    def pair_left_identity_law(
        pair: Tuple[_FirstType, _SecondType],
        container: 'PairLikeN[_FirstType, _SecondType, _ThirdType]',
        function: Callable[
            [_FirstType, _SecondType],
            KindN['PairLikeN', _NewFirstType, _NewSecondType, _ThirdType],
        ],
    ) -> None:
        """Ensures that unpaired and paired constructors work fine."""
        assert_equal(
            container.from_paired(*pair).pair(function),
            function(*pair),
        )


class PairLikeN(
    bindable.BindableN[_FirstType, _SecondType, _ThirdType],
    swappable.SwappableN[_FirstType, _SecondType, _ThirdType],
    lashable.LashableN[_FirstType, _SecondType, _ThirdType],
    equable.Equable,
):
    """Special interface for types that look like a ``Pair``."""

    _laws: ClassVar[Sequence[Law]] = (
        Law2(_LawSpec.pair_equality_law),
        Law3(_LawSpec.pair_left_identity_law),
    )

    @abstractmethod
    def pair(
        self: _PairLikeKind,
        function: Callable[
            [_FirstType, _SecondType],
            KindN[_PairLikeKind, _NewFirstType, _NewSecondType, _ThirdType],
        ],
    ) -> KindN[_PairLikeKind, _NewFirstType, _NewSecondType, _ThirdType]:
        """Allows to work with both arguments at the same time."""

    @classmethod
    @abstractmethod
    def from_paired(
        cls: Type[_PairLikeKind],
        first: _NewFirstType,
        second: _NewSecondType,
    ) -> KindN[_PairLikeKind, _NewFirstType, _NewSecondType, _ThirdType]:
        """Allows to create a PairLikeN from just two values."""

    @classmethod
    @abstractmethod
    def from_unpaired(
        cls: Type[_PairLikeKind],
        inner_value: _NewFirstType,
    ) -> KindN[_PairLikeKind, _NewFirstType, _NewFirstType, _ThirdType]:
        """Allows to create a PairLikeN from just a single object."""


PairLike2 = PairLikeN[_FirstType, _SecondType, NoReturn]
PairLike3 = PairLikeN[_FirstType, _SecondType, _ThirdType]


@final
class Pair(
    BaseContainer,
    SupportsKind2['Pair', _FirstType, _SecondType],
    PairLike2[_FirstType, _SecondType],
):
    """
    A type that represents a pair of something.

    Like to coordinates ``(x, y)`` or two best friends.
    Or a question and an answer.

    """

    def __init__(
        self,
        inner_value: Tuple[_FirstType, _SecondType],
    ) -> None:
        """Saves passed tuple as ``._inner_value`` inside this instance."""
        super().__init__(inner_value)

    # `Equable` part:

    equals = container_equality  # we already have this defined for all types

    # `Mappable` part via `BiMappable`:

    def map(  # noqa: WPS125
        self,
        function: Callable[[_FirstType], _NewFirstType],
    ) -> 'Pair[_NewFirstType, _SecondType]':
        """
        Changes the first type with a pure function.

        >>> assert Pair((1, 2)).map(str) == Pair(('1', 2))

        """
        return Pair((function(self._inner_value[0]), self._inner_value[1]))

    # `BindableN` part:

    def bind(
        self,
        function: Callable[
            [_FirstType],
            Kind2['Pair', _NewFirstType, _SecondType],
        ],
    ) -> 'Pair[_NewFirstType, _SecondType]':
        """
        Changes the first type with a function returning another ``Pair``.

        >>> def bindable(first: int) -> Pair[str, str]:
        ...     return Pair((str(first), ''))

        >>> assert Pair((1, 'b')).bind(bindable) == Pair(('1', ''))

        """
        return dekind(function(self._inner_value[0]))

    # `AltableN` part via `BiMappableN`:

    def alt(
        self,
        function: Callable[[_SecondType], _NewSecondType],
    ) -> 'Pair[_FirstType, _NewSecondType]':
        """
        Changes the second type with a pure function.

        >>> assert Pair((1, 2)).alt(str) == Pair((1, '2'))

        """
        return Pair((self._inner_value[0], function(self._inner_value[1])))

    # `LashableN` part:

    def lash(
        self,
        function: Callable[
            [_SecondType],
            Kind2['Pair', _FirstType, _NewSecondType],
        ],
    ) -> 'Pair[_FirstType, _NewSecondType]':
        """
        Changes the second type with a function returning ``Pair``.

        >>> def lashable(second: int) -> Pair[str, str]:
        ...     return Pair(('', str(second)))

        >>> assert Pair(('a', 2)).lash(lashable) == Pair(('', '2'))

        """
        return dekind(function(self._inner_value[1]))

    # `SwappableN` part:

    def swap(self) -> 'Pair[_SecondType, _FirstType]':
        """
        Swaps ``Pair`` elements.

        >>> assert Pair((1, 2)).swap() == Pair((2, 1))

        """
        return Pair((self._inner_value[1], self._inner_value[0]))

    # `PairLikeN` part:

    def pair(
        self,
        function: Callable[
            [_FirstType, _SecondType],
            Kind2['Pair', _NewFirstType, _NewSecondType],
        ],
    ) -> 'Pair[_NewFirstType, _NewSecondType]':
        """
        Creates a new ``Pair`` from an existing one via a passed function.

        >>> def min_max(first: int, second: int) -> Pair[int, int]:
        ...     return Pair((min(first, second), max(first, second)))

        >>> assert Pair((2, 1)).pair(min_max) == Pair((1, 2))
        >>> assert Pair((1, 2)).pair(min_max) == Pair((1, 2))

        """
        return dekind(function(self._inner_value[0], self._inner_value[1]))

    @classmethod
    def from_paired(
        cls,
        first: _NewFirstType,
        second: _NewSecondType,
    ) -> 'Pair[_NewFirstType, _NewSecondType]':
        """
        Creates a new pair from two values.

        >>> assert Pair.from_paired(1, 2) == Pair((1, 2))

        """
        return Pair((first, second))

    @classmethod
    def from_unpaired(
        cls,
        inner_value: _NewFirstType,
    ) -> 'Pair[_NewFirstType, _NewFirstType]':
        """
        Creates a new pair from a single value.

        >>> assert Pair.from_unpaired(1) == Pair((1, 1))

        """
        return Pair((inner_value, inner_value))


# Running hypothesis auto-generated tests:
check_all_laws(Pair, use_init=True)

Step 6: Writing type-tests

Note

You can find all type-tests here.

The next thing we want is to write a type-test!

What is a type-test? This is a special type of tests for your typing. We run mypy on top of tests and use snapshots to assert the result.

We recommend to use pytest-mypy-plugins. Read more about how to use it.

Let’s start with a simple test to make sure our .pair function works correctly:

Warning

Please, don’t use env: property the way we do here. We need it since we store our example in tests/ folder. And we have to tell mypy how to find it.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
- case: test_pair_type
  disable_cache: false
  env:
    # We only need this because we store this example in `tests/`
    # and not in our source code. Please, do not copy this line!
    - MYPYPATH=./tests/test_examples/test_your_container
  main: |
    # Let's import our `Pair` type we defined earlier:
    from test_pair4 import Pair

    def function(first: int, second: str) -> Pair[float, bool]:
        ...

    my_pair: Pair[int, str] = Pair.from_paired(1, 'a')
    reveal_type(my_pair.pair(function))
  out: |
    main:8: note: Revealed type is 'test_pair4.Pair[builtins.float*, builtins.bool*]'

Ok, now, let’s try to raise an error by using it incorrectly:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
- case: test_pair_error
  disable_cache: false
  env:
    # We only need this because we store this example in `tests/`
    # and not in our source code. Please, do not copy this line!
    - MYPYPATH=./tests/test_examples/test_your_container
  main: |
    # Let's import our `Pair` type we defined earlier:
    from test_pair4 import Pair

    # Oups! This function has first and second types swapped!
    def function(first: str, second: int) -> Pair[float, bool]:
        ...

    my_pair = Pair.from_paired(1, 'a')
    my_pair.pair(function)  # this should and will error
  out: |
    main:9: error: Argument 1 to "pair" of "Pair" has incompatible type "Callable[[str, int], Pair[float, bool]]"; expected "Callable[[int, str], KindN[Pair[Any, Any], float, bool, Any]]"

Step 7: Reusing code

The last (but not the least!) thing you need to know is that you can reuse all code we already have for this new Pair type.

This is because of our Higher Kinded Types feature.

So, let’s say we want to use native map_() pointfree function with our new Pair type. Let’s test that it will work correctly:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
- case: test_pair_map
  disable_cache: false
  env:
    # We only need this because we store this example in `tests/`
    # and not in our source code. Please, do not copy this line!
    - MYPYPATH=./tests/test_examples/test_your_container
  main: |
    from test_pair4 import Pair
    from returns.pointfree import map_

    my_pair: Pair[int, int] = Pair.from_unpaired(1)
    reveal_type(my_pair.map(str))
    reveal_type(map_(str)(my_pair))
  out: |
    main:5: note: Revealed type is 'test_pair4.Pair[builtins.str*, builtins.int]'
    main:6: note: Revealed type is 'test_pair4.Pair[builtins.str, builtins.int]'

Yes, it works!

Now you have fully working, typed, documented, lawful, and tested primitive. You can build any other primitive you need for your business logic or infrastructure.

Context Pipelines