A RetroSearch Logo

Home - News ( United States | United Kingdom | Italy | Germany ) - Football scores

Search Query:

Showing content from https://returns.readthedocs.io/en/latest/pages/create-your-own-container.html below:

Create your own container - returns 0.26.0 documentation

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.

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 features, 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.

 1from collections.abc import Callable
 2from typing import TypeVar, final
 3
 4from returns.interfaces import bindable, equable, lashable, swappable
 5from returns.primitives.container import BaseContainer, container_equality
 6from returns.primitives.hkt import Kind2, SupportsKind2, dekind
 7
 8_FirstType = TypeVar('_FirstType')
 9_SecondType = TypeVar('_SecondType')
10
11_NewFirstType = TypeVar('_NewFirstType')
12_NewSecondType = TypeVar('_NewSecondType')
13
14
15@final
16class Pair(
17    BaseContainer,
18    SupportsKind2['Pair', _FirstType, _SecondType],
19    bindable.Bindable2[_FirstType, _SecondType],
20    swappable.Swappable2[_FirstType, _SecondType],
21    lashable.Lashable2[_FirstType, _SecondType],
22    equable.Equable,
23):
24    """
25    A type that represents a pair of something.
26
27    Like to coordinates ``(x, y)`` or two best friends.
28    Or a question and an answer.
29
30    """
31
32    def __init__(
33        self,
34        inner_value: tuple[_FirstType, _SecondType],
35    ) -> None:
36        """Saves passed tuple as ``._inner_value`` inside this instance."""
37        super().__init__(inner_value)
38
39    # `Equable` part:
40
41    equals = container_equality  # we already have this defined for all types
42
43    # `Mappable` part via `BiMappable`:
44
45    def map(
46        self,
47        function: Callable[[_FirstType], _NewFirstType],
48    ) -> 'Pair[_NewFirstType, _SecondType]':
49        return Pair((function(self._inner_value[0]), self._inner_value[1]))
50
51    # `BindableN` part:
52
53    def bind(
54        self,
55        function: Callable[
56            [_FirstType],
57            Kind2['Pair', _NewFirstType, _SecondType],
58        ],
59    ) -> 'Pair[_NewFirstType, _SecondType]':
60        return dekind(function(self._inner_value[0]))
61
62    # `AltableN` part via `BiMappableN`:
63
64    def alt(
65        self,
66        function: Callable[[_SecondType], _NewSecondType],
67    ) -> 'Pair[_FirstType, _NewSecondType]':
68        return Pair((self._inner_value[0], function(self._inner_value[1])))
69
70    # `LashableN` part:
71
72    def lash(
73        self,
74        function: Callable[
75            [_SecondType],
76            Kind2['Pair', _FirstType, _NewSecondType],
77        ],
78    ) -> 'Pair[_FirstType, _NewSecondType]':
79        return dekind(function(self._inner_value[1]))
80
81    # `SwappableN` part:
82
83    def swap(self) -> 'Pair[_SecondType, _FirstType]':
84        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:

 1class PairLikeN(
 2    bindable.BindableN[_FirstType, _SecondType, _ThirdType],
 3    swappable.SwappableN[_FirstType, _SecondType, _ThirdType],
 4    lashable.LashableN[_FirstType, _SecondType, _ThirdType],
 5    equable.Equable,
 6):
 7    """Special interface for types that look like a ``Pair``."""
 8
 9    @abstractmethod
10    def pair(
11        self: _PairLikeKind,
12        function: Callable[
13            [_FirstType, _SecondType],
14            KindN[_PairLikeKind, _NewFirstType, _NewSecondType, _ThirdType],
15        ],
16    ) -> KindN[_PairLikeKind, _NewFirstType, _NewSecondType, _ThirdType]:
17        """Allows to work with both arguments at the same time."""
18
19    @classmethod
20    @abstractmethod
21    def from_paired(
22        cls: type[_PairLikeKind],
23        first: _NewFirstType,
24        second: _NewSecondType,
25    ) -> KindN[_PairLikeKind, _NewFirstType, _NewSecondType, _ThirdType]:
26        """Allows to create a PairLikeN from just two values."""
27
28    @classmethod
29    @abstractmethod
30    def from_unpaired(
31        cls: type[_PairLikeKind],
32        inner_value: _NewFirstType,
33    ) -> KindN[_PairLikeKind, _NewFirstType, _NewFirstType, _ThirdType]:
34        """Allows to create a PairLikeN from just a single object."""

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

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

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

 1    def map(
 2        self,
 3        function: Callable[[_FirstType], _NewFirstType],
 4    ) -> 'Pair[_NewFirstType, _SecondType]':
 5        """
 6        Changes the first type with a pure function.
 7
 8        >>> assert Pair((1, 2)).map(str) == Pair(('1', 2))
 9
10        """
11        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:

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

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

 1class PairLikeN(
 2    bindable.BindableN[_FirstType, _SecondType, _ThirdType],
 3    swappable.SwappableN[_FirstType, _SecondType, _ThirdType],
 4    lashable.LashableN[_FirstType, _SecondType, _ThirdType],
 5    equable.Equable,
 6):
 7    """Special interface for types that look like a ``Pair``."""
 8
 9    _laws: ClassVar[Sequence[Law]] = (
10        Law2(_LawSpec.pair_equality_law),
11        Law3(_LawSpec.pair_left_identity_law),
12    )
13
14    @abstractmethod
15    def pair(
16        self: _PairLikeKind,
17        function: Callable[
18            [_FirstType, _SecondType],
19            KindN[_PairLikeKind, _NewFirstType, _NewSecondType, _ThirdType],
20        ],
21    ) -> KindN[_PairLikeKind, _NewFirstType, _NewSecondType, _ThirdType]:
22        """Allows to work with both arguments at the same time."""
23
24    @classmethod
25    @abstractmethod
26    def from_paired(
27        cls: type[_PairLikeKind],
28        first: _NewFirstType,
29        second: _NewSecondType,
30    ) -> KindN[_PairLikeKind, _NewFirstType, _NewSecondType, _ThirdType]:
31        """Allows to create a PairLikeN from just two values."""
32
33    @classmethod
34    @abstractmethod
35    def from_unpaired(
36        cls: type[_PairLikeKind],
37        inner_value: _NewFirstType,
38    ) -> KindN[_PairLikeKind, _NewFirstType, _NewFirstType, _ThirdType]:
39        """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:

  1from abc import abstractmethod
  2from collections.abc import Callable, Sequence
  3from typing import ClassVar, TypeVar, final
  4
  5from typing_extensions import Never
  6
  7from returns.contrib.hypothesis.laws import check_all_laws
  8from returns.interfaces import bindable, equable, lashable, swappable
  9from returns.primitives.asserts import assert_equal
 10from returns.primitives.container import BaseContainer, container_equality
 11from returns.primitives.hkt import Kind2, KindN, SupportsKind2, dekind
 12from returns.primitives.laws import Law, Law2, Law3, LawSpecDef, law_definition
 13
 14_FirstType = TypeVar('_FirstType')
 15_SecondType = TypeVar('_SecondType')
 16_ThirdType = TypeVar('_ThirdType')
 17
 18_NewFirstType = TypeVar('_NewFirstType')
 19_NewSecondType = TypeVar('_NewSecondType')
 20
 21_PairLikeKind = TypeVar('_PairLikeKind', bound='PairLikeN')
 22
 23
 24class _LawSpec(LawSpecDef):
 25    @law_definition
 26    def pair_equality_law(
 27        raw_value: _FirstType,
 28        container: 'PairLikeN[_FirstType, _SecondType, _ThirdType]',
 29    ) -> None:
 30        """Ensures that unpaired and paired constructors work fine."""
 31        assert_equal(
 32            container.from_unpaired(raw_value),
 33            container.from_paired(raw_value, raw_value),
 34        )
 35
 36    @law_definition
 37    def pair_left_identity_law(
 38        pair: tuple[_FirstType, _SecondType],
 39        container: 'PairLikeN[_FirstType, _SecondType, _ThirdType]',
 40        function: Callable[
 41            [_FirstType, _SecondType],
 42            KindN['PairLikeN', _NewFirstType, _NewSecondType, _ThirdType],
 43        ],
 44    ) -> None:
 45        """Ensures that unpaired and paired constructors work fine."""
 46        assert_equal(
 47            container.from_paired(*pair).pair(function),
 48            function(*pair),
 49        )
 50
 51
 52class PairLikeN(
 53    bindable.BindableN[_FirstType, _SecondType, _ThirdType],
 54    swappable.SwappableN[_FirstType, _SecondType, _ThirdType],
 55    lashable.LashableN[_FirstType, _SecondType, _ThirdType],
 56    equable.Equable,
 57):
 58    """Special interface for types that look like a ``Pair``."""
 59
 60    _laws: ClassVar[Sequence[Law]] = (
 61        Law2(_LawSpec.pair_equality_law),
 62        Law3(_LawSpec.pair_left_identity_law),
 63    )
 64
 65    @abstractmethod
 66    def pair(
 67        self: _PairLikeKind,
 68        function: Callable[
 69            [_FirstType, _SecondType],
 70            KindN[_PairLikeKind, _NewFirstType, _NewSecondType, _ThirdType],
 71        ],
 72    ) -> KindN[_PairLikeKind, _NewFirstType, _NewSecondType, _ThirdType]:
 73        """Allows to work with both arguments at the same time."""
 74
 75    @classmethod
 76    @abstractmethod
 77    def from_paired(
 78        cls: type[_PairLikeKind],
 79        first: _NewFirstType,
 80        second: _NewSecondType,
 81    ) -> KindN[_PairLikeKind, _NewFirstType, _NewSecondType, _ThirdType]:
 82        """Allows to create a PairLikeN from just two values."""
 83
 84    @classmethod
 85    @abstractmethod
 86    def from_unpaired(
 87        cls: type[_PairLikeKind],
 88        inner_value: _NewFirstType,
 89    ) -> KindN[_PairLikeKind, _NewFirstType, _NewFirstType, _ThirdType]:
 90        """Allows to create a PairLikeN from just a single object."""
 91
 92
 93PairLike2 = PairLikeN[_FirstType, _SecondType, Never]
 94PairLike3 = PairLikeN[_FirstType, _SecondType, _ThirdType]
 95
 96
 97@final
 98class Pair(
 99    BaseContainer,
100    SupportsKind2['Pair', _FirstType, _SecondType],
101    PairLike2[_FirstType, _SecondType],
102):
103    """
104    A type that represents a pair of something.
105
106    Like to coordinates ``(x, y)`` or two best friends.
107    Or a question and an answer.
108
109    """
110
111    def __init__(
112        self,
113        inner_value: tuple[_FirstType, _SecondType],
114    ) -> None:
115        """Saves passed tuple as ``._inner_value`` inside this instance."""
116        super().__init__(inner_value)
117
118    # `Equable` part:
119
120    equals = container_equality  # we already have this defined for all types
121
122    # `Mappable` part via `BiMappable`:
123
124    def map(
125        self,
126        function: Callable[[_FirstType], _NewFirstType],
127    ) -> 'Pair[_NewFirstType, _SecondType]':
128        """
129        Changes the first type with a pure function.
130
131        >>> assert Pair((1, 2)).map(str) == Pair(('1', 2))
132
133        """
134        return Pair((function(self._inner_value[0]), self._inner_value[1]))
135
136    # `BindableN` part:
137
138    def bind(
139        self,
140        function: Callable[
141            [_FirstType],
142            Kind2['Pair', _NewFirstType, _SecondType],
143        ],
144    ) -> 'Pair[_NewFirstType, _SecondType]':
145        """
146        Changes the first type with a function returning another ``Pair``.
147
148        >>> def bindable(first: int) -> Pair[str, str]:
149        ...     return Pair((str(first), ''))
150
151        >>> assert Pair((1, 'b')).bind(bindable) == Pair(('1', ''))
152
153        """
154        return dekind(function(self._inner_value[0]))
155
156    # `AltableN` part via `BiMappableN`:
157
158    def alt(
159        self,
160        function: Callable[[_SecondType], _NewSecondType],
161    ) -> 'Pair[_FirstType, _NewSecondType]':
162        """
163        Changes the second type with a pure function.
164
165        >>> assert Pair((1, 2)).alt(str) == Pair((1, '2'))
166
167        """
168        return Pair((self._inner_value[0], function(self._inner_value[1])))
169
170    # `LashableN` part:
171
172    def lash(
173        self,
174        function: Callable[
175            [_SecondType],
176            Kind2['Pair', _FirstType, _NewSecondType],
177        ],
178    ) -> 'Pair[_FirstType, _NewSecondType]':
179        """
180        Changes the second type with a function returning ``Pair``.
181
182        >>> def lashable(second: int) -> Pair[str, str]:
183        ...     return Pair(('', str(second)))
184
185        >>> assert Pair(('a', 2)).lash(lashable) == Pair(('', '2'))
186
187        """
188        return dekind(function(self._inner_value[1]))
189
190    # `SwappableN` part:
191
192    def swap(self) -> 'Pair[_SecondType, _FirstType]':
193        """
194        Swaps ``Pair`` elements.
195
196        >>> assert Pair((1, 2)).swap() == Pair((2, 1))
197
198        """
199        return Pair((self._inner_value[1], self._inner_value[0]))
200
201    # `PairLikeN` part:
202
203    def pair(
204        self,
205        function: Callable[
206            [_FirstType, _SecondType],
207            Kind2['Pair', _NewFirstType, _NewSecondType],
208        ],
209    ) -> 'Pair[_NewFirstType, _NewSecondType]':
210        """
211        Creates a new ``Pair`` from an existing one via a passed function.
212
213        >>> def min_max(first: int, second: int) -> Pair[int, int]:
214        ...     return Pair((min(first, second), max(first, second)))
215
216        >>> assert Pair((2, 1)).pair(min_max) == Pair((1, 2))
217        >>> assert Pair((1, 2)).pair(min_max) == Pair((1, 2))
218
219        """
220        return dekind(function(self._inner_value[0], self._inner_value[1]))
221
222    @classmethod
223    def from_paired(
224        cls,
225        first: _NewFirstType,
226        second: _NewSecondType,
227    ) -> 'Pair[_NewFirstType, _NewSecondType]':
228        """
229        Creates a new pair from two values.
230
231        >>> assert Pair.from_paired(1, 2) == Pair((1, 2))
232
233        """
234        return Pair((first, second))
235
236    @classmethod
237    def from_unpaired(
238        cls,
239        inner_value: _NewFirstType,
240    ) -> 'Pair[_NewFirstType, _NewFirstType]':
241        """
242        Creates a new pair from a single value.
243
244        >>> assert Pair.from_unpaired(1) == Pair((1, 1))
245
246        """
247        return Pair((inner_value, inner_value))
248
249
250# Running hypothesis auto-generated tests:
251check_all_laws(Pair, use_init=True)
Step 6: Writing type-tests

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- case: test_pair_type
 2  disable_cache: false
 3  env:
 4  - MYPYPATH=./tests/test_examples/test_your_container
 5  mypy_config: disallow_subclassing_any = False
 6  main: |
 7    # Let's import our `Pair` type we defined earlier:
 8    from test_pair4 import Pair
 9
10    reveal_type(Pair)
11
12    def function(first: int, second: str) -> Pair[float, bool]:
13        ...
14
15    my_pair: Pair[int, str] = Pair.from_paired(1, 'a')
16    reveal_type(my_pair.pair(function))
17  out: |
18    main:4: note: Revealed type is "def [_FirstType, _SecondType] (inner_value: tuple[_FirstType`1, _SecondType`2]) -> test_pair4.Pair[_FirstType`1, _SecondType`2]"
19    main:10: 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- case: test_pair_error
 2  disable_cache: false
 3  env:
 4    # We only need this because we store this example in `tests/`
 5    # and not in our source code. Please, do not copy this line!
 6    - MYPYPATH=./tests/test_examples/test_your_container
 7
 8  # TODO: remove this config after
 9  #     mypy/typeshed/stdlib/unittest/mock.pyi:120:
10  #     error: Class cannot subclass "Any" (has type "Any")
11  # is fixed.
12  mypy_config:
13    disallow_subclassing_any = False
14  main: |
15    # Let's import our `Pair` type we defined earlier:
16    from test_pair4 import Pair
17
18    # Oups! This function has first and second types swapped!
19    def function(first: str, second: int) -> Pair[float, bool]:
20        ...
21
22    my_pair = Pair.from_paired(1, 'a')
23    my_pair.pair(function)  # this should and will error
24  out: |
25    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]]"  [arg-type]
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- case: test_pair_map
 2  disable_cache: false
 3  env:
 4  - MYPYPATH=./tests/test_examples/test_your_container
 5  mypy_config: disallow_subclassing_any = False
 6  main: |
 7    from test_pair4 import Pair
 8    from returns.pointfree import map_
 9
10    my_pair: Pair[int, int] = Pair.from_unpaired(1)
11    reveal_type(my_pair.map(str))
12    reveal_type(map_(str)(my_pair))
13  out: |
14    main:5: note: Revealed type is "test_pair4.Pair[builtins.str, builtins.int]"
15    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.


RetroSearch is an open source project built by @garambo | Open a GitHub Issue

Search and Browse the WWW like it's 1997 | Search results from DuckDuckGo

HTML: 3.2 | Encoding: UTF-8 | Version: 0.7.4