@@ -30,6 +30,9 @@ What is a ``Pair``? Well, it is literally a pair of two values.
30
30
No more, no less. Similar to a ``Tuple[FirstType, SecondType] ``.
31
31
But with extra goodies.
32
32
33
+ .. note ::
34
+ You can find all `code samples here <https://github.com/dry-python/returns/tree/master/tests/test_examples >`_.
35
+
33
36
34
37
Step 1: Choosing right interfaces
35
38
---------------------------------
@@ -52,19 +55,16 @@ And then subtype the interfaces that provide these methods and laws.
52
55
53
56
What interfaces a ``Pair `` type needs?
54
57
55
- - :class: `returns.interfaces.equable.SupportsEquality `,
58
+ - :class: `returns.interfaces.equable.Equable `,
56
59
because two ``Pair `` instances can be compared
57
60
- :class: `returns.interfaces.mappable.MappableN `,
58
61
because the first type can be composed with pure functions
59
62
- :class: `returns.interfaces.bindable.BindableN `,
60
63
because a ``Pair `` can be bound to a function returning a new ``Pair ``
61
64
based on the first type
62
- - :class: `returns.interfaces.applicative.ApplicativeN `,
63
- because you can construct new ``Piar `` from a value
64
- and apply a wrapped function over it
65
65
- :class: `returns.interfaces.altable.AltableN `,
66
66
because the second type can be composed with pure functions
67
- - :class: `returns.interfaces.rescuable.RescuableN `,
67
+ - :class: `returns.interfaces.lashable.LashableN `,
68
68
because a ``Pair `` can be bound to a function returning a new ``Pair ``
69
69
based on the second type
70
70
@@ -74,9 +74,8 @@ let's find pre-defined aliases we can reuse.
74
74
Turns out, there are some of them!
75
75
76
76
- :class: `returns.interfaces.bimappable.BiMappableN `
77
- which combines ``MappableN `` and ``AltableN ``
78
- - :class: `returns.interfaces.container.ContainerN ` which combines
79
- ``ApplicativeN `` and ``BindableN ``
77
+ which combines ``MappableN `` and ``AltableN ``,
78
+ it also requires to add a new method called ``.swap `` to change values order
80
79
81
80
Let's look at the resul:
82
81
@@ -85,8 +84,8 @@ Let's look at the resul:
85
84
>>> from typing_extensions import final
86
85
>>> from typing import Callable, TypeVar, Tuple
87
86
88
- >>> from returns.interfaces import container, bimappable, rescuable , equable
89
- >>> from returns.primitives.container import BaseContainer, container_equality
87
+ >>> from returns.interfaces import bimappable, bindable , equable, lashable
88
+ >>> from returns.primitives.container import BaseContainer
90
89
>>> from returns.primitives.hkt import SupportsKind2
91
90
92
91
>>> _FirstType = TypeVar('_FirstType')
@@ -99,10 +98,10 @@ Let's look at the resul:
99
98
... class Pair(
100
99
... BaseContainer,
101
100
... SupportsKind2['Pair', _FirstType, _SecondType],
102
- ... container.Container2 [_FirstType, _SecondType],
101
+ ... bindable.Bindable2 [_FirstType, _SecondType],
103
102
... bimappable.BiMappable2[_FirstType, _SecondType],
104
- ... rescuable.Rescuable2 [_FirstType, _SecondType],
105
- ... equable.SupportsEquality ,
103
+ ... lashable.Lashable2 [_FirstType, _SecondType],
104
+ ... equable.Equable ,
106
105
... ):
107
106
... def __init__(
108
107
... self, inner_value: Tuple[_FirstType, _SecondType],
@@ -119,36 +118,171 @@ Let's look at the resul:
119
118
Later we will talk about an actual implementation of all required methods.
120
119
121
120
122
- Step 2: Defining new interfaces and associated laws
123
- ---------------------------------------------------
121
+ Step 2: Initial implementation
122
+ ------------------------------
123
+
124
+ So, let's start writting some code!
125
+
126
+ We would need to implement all interface methods,
127
+ otherwise ``mypy `` won't be happy.
128
+ That's what it currently says on our type definition:
129
+
130
+ .. code ::
131
+
132
+ error: Final class test_pair1.Pair has abstract attributes "alt", "bind", "equals", "lash", "map", "swap"
133
+
134
+ Looks like it already knows what methods should be there!
135
+
136
+ Ok, let's drop some initial and straight forward implementation.
137
+ We will later make it more complex step by step.
138
+
139
+ .. literalinclude :: ../../tests/test_examples/test_pair1.py
140
+ :linenos:
141
+
142
+ You can check our resulting source with ``mypy ``. It would be happy this time.
143
+
144
+
145
+ Step 3: New interfaces
146
+ ----------------------
147
+
148
+ As you can see our existing interfaces do not cover everything.
149
+ We can potentially want several extra things:
124
150
125
- After the initial analysis in the "Step 1",
126
- you can decide to introduce your own methods.
151
+ 1. A method that takes two arguments and returns a new ``Pair `` instance
152
+ 2. A named constructor to create a ``Pair `` from a single value
153
+ 3. A named constructor to create a ``Pair `` from two values
127
154
128
- These methods can probably form an interface,
129
- if you want to make generic utilities for your type .
155
+ We can define an interface just for this!
156
+ It would be also nice to add all other interfaces there as supertypes .
130
157
131
- Let's say your type will have ``.from_tuple `` and ``.replace `` methods,
132
- that can look like so:
158
+ That's how it is going to look:
133
159
160
+ .. literalinclude :: ../../tests/test_examples/test_pair2.py
161
+ :linenos:
162
+ :pyobject: PairLikeN
134
163
164
+ Awesome! Now we have a new interface to implement. Let's do that!
135
165
166
+ .. literalinclude :: ../../tests/test_examples/test_pair2.py
167
+ :linenos:
168
+ :pyobject: Pair.pair
136
169
137
- Step 3: Actual implementation
138
- -----------------------------
170
+ .. literalinclude :: ../../tests/test_examples/test_pair2.py
171
+ :linenos:
172
+ :pyobject: Pair.from_unpaired
173
+
174
+ Looks like we are done!
139
175
140
176
141
177
Step 4: Writting tests and docs
142
178
-------------------------------
143
179
180
+ The best part about this type is that it is pure.
181
+ So, we can write our tests inside docs!
144
182
145
- Step 5: Writting type-tests
146
- ---------------------------
183
+ We are going to use
184
+ `doctests <https://docs.python.org/3/library/doctest.html >`_
185
+ builtin module for that.
186
+
187
+ This gives us several key benefits:
188
+
189
+ - All our docs has usage examples
190
+ - All our examples are correct, because they are executed and tested
191
+ - We don't need to write regular boring tests
192
+
193
+ Let's add docs and doctests! Let's use ``map `` method as a short example:
194
+
195
+ .. literalinclude :: ../../tests/test_examples/test_pair3.py
196
+ :linenos:
197
+ :pyobject: Pair.map
147
198
199
+ By adding these simple tests we would already have 100% coverage.
200
+ But, what if we can completely skip writting tests, but still have 100%?
148
201
149
- Step 6: Checking laws
202
+ Let's discuss how we can achieve that with "Laws as values".
203
+
204
+
205
+ Step 5: Checking laws
150
206
---------------------
151
207
208
+ We already ship lots of laws with our interfaces.
209
+ See our docs on :ref: `laws and checking them <hypothesis-plugins >`.
210
+
211
+ Moreover, you can also define your own laws!
212
+ Let's add them to our ``PairLikeN `` interface.
213
+
214
+ Let's start with laws definition:
215
+
216
+ .. literalinclude :: ../../tests/test_examples/test_pair4.py
217
+ :linenos:
218
+ :pyobject: _LawSpec
219
+
220
+ And them let's add them to our ``PairLikeN `` interface:
221
+
222
+ .. literalinclude :: ../../tests/test_examples/test_pair4.py
223
+ :linenos:
224
+ :pyobject: PairLikeN
225
+ :emphasize-lines: 9-12
226
+
227
+ The last to do is to call ``check_all_laws(Pair, use_init=True) ``
228
+ to generate 10 ``hypothesis `` test cases with hundreds real test cases inside.
229
+
230
+ Here's the final result of our brand new ``Pair `` type:
231
+
232
+ .. literalinclude :: ../../tests/test_examples/test_pair4.py
233
+ :linenos:
234
+
235
+
236
+ Step 6: Writting type-tests
237
+ ---------------------------
238
+
239
+ .. note ::
240
+ You can find all `type-tests here <https://github.com/dry-python/returns/tree/master/typesafety/test_examples >`_.
241
+
242
+ The next thing we want is to write a type-test!
243
+
244
+ What is a type-test? This is a special type of tests for your typing.
245
+ We run ``mypy `` on top of tests and use snapshots to assert the result.
246
+
247
+ We recommend to use `pytest-mypy-plugins <https://github.com/typeddjango/pytest-mypy-plugins >`_.
248
+ `Read more <https://sobolevn.me/2019/08/testing-mypy-types >`_
249
+ about how to use it.
250
+
251
+ Let's start with a simple test
252
+ to make sure our ``.pair `` function works correctly:
253
+
254
+ .. warning ::
255
+ Please, don't use ``env: `` property the way we do here.
256
+ We need it since we store our example in ``tests/ `` folder.
257
+ And we have to tell ``mypy `` how to find it.
258
+
259
+ .. literalinclude :: ../../typesafety/test_examples/test_pair4_def.yml
260
+ :linenos:
261
+
262
+ Ok, now, let's try to raise an error by using it incorrectly:
263
+
264
+ .. literalinclude :: ../../typesafety/test_examples/test_pair4_error.yml
265
+ :linenos:
266
+
152
267
153
268
Step 7: Reusing code
154
269
--------------------
270
+
271
+ The last (but not the least!) thing you need
272
+ to know is that you can reuse all code
273
+ we already have for this new ``Pair `` type.
274
+
275
+ This is because of our :ref: `hkt ` feature.
276
+
277
+ So, let's say we want to use native :func: `~returns.pointfree.map.map_ `
278
+ pointfree function with our new ``Pair `` type.
279
+ Let's test that it will work correctly:
280
+
281
+ .. literalinclude :: ../../typesafety/test_examples/test_pair4_reuse.yml
282
+ :linenos:
283
+
284
+ Yes, it works!
285
+
286
+ Now you have fully working, typed, documented, lawful, and tested primitive.
287
+ You can build any other primitive
288
+ you need for your business logic or infrastructure.
0 commit comments