Skip to content

Commit cd08c2a

Browse files
authored
Adds do-notation (#1298)
* Initial commit * Fixes tests * Fix CI * Fix CI * Fix CI
1 parent af347f4 commit cd08c2a

File tree

21 files changed

+1289
-238
lines changed

21 files changed

+1289
-238
lines changed

.github/workflows/test.yml

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,13 +11,14 @@ on:
1111
- 'docs/requirements.txt'
1212
workflow_dispatch:
1313

14-
concurrency:
15-
group: ${{ github.head_ref || github.run_id }}
14+
concurrency:
15+
group: test-${{ github.head_ref || github.run_id }}
1616
cancel-in-progress: true
1717

1818
jobs:
1919
build:
2020
runs-on: ubuntu-latest
21+
fail-fast: false
2122
strategy:
2223
matrix:
2324
python-version: ['3.7', '3.8', '3.9', '3.10']
@@ -73,7 +74,7 @@ jobs:
7374
poetry run pytest typesafety -p no:cov -o addopts="" --mypy-ini-file=setup.cfg
7475
7576
- name: Upload coverage to Codecov
76-
if: matrix.python-version == 3.8
77+
if: ${{ matrix.python-version }} == 3.8
7778
uses: codecov/codecov-action@v2.1.0
7879
with:
7980
file: ./coverage.xml

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,12 @@ Versions before `1.0.0` are `0Ver`-based:
55
incremental in minor, bugfixes only are patches.
66
See [0Ver](https://0ver.org/).
77

8+
89
## 0.19.0 WIP
910

1011
### Features
1112

13+
- Adds `do` notation
1214
- Adds `attempt` decorator
1315

1416
### Misc

docs/index.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ Contents
3636
pages/converters.rst
3737
pages/pointfree.rst
3838
pages/methods.rst
39+
pages/do-notation.rst
3940
pages/functions.rst
4041
pages/curry.rst
4142
pages/types.rst

docs/pages/contrib/mypy_plugins.rst

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,3 +126,12 @@ Pipe
126126

127127
.. automodule:: returns.contrib.mypy._features.pipe
128128
:members:
129+
130+
Do notation
131+
~~~~~~~~~~~
132+
133+
.. autoclasstree:: returns.contrib.mypy._features.do_notation
134+
:strict:
135+
136+
.. automodule:: returns.contrib.mypy._features.do_notation
137+
:members:

docs/pages/do-notation.rst

Lines changed: 221 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,221 @@
1+
.. _do-notation:
2+
3+
Do Notation
4+
===========
5+
6+
.. note::
7+
8+
Technical note: this feature requires :ref:`mypy plugin <mypy-plugins>`.
9+
10+
All containers can be easily composed
11+
with functions that can take a single argument.
12+
13+
But, what if we need to compose two containers
14+
with a function with two arguments?
15+
That's not so easy.
16+
17+
Of course, we can use :ref:`curry` and ``.apply`` or some imperative code.
18+
But, it is not very easy to write and read.
19+
20+
This is why multiple functional languages have a concept of "do-notation".
21+
It allows you to write beautiful imperative code.
22+
23+
24+
Regular containers
25+
------------------
26+
27+
Let's say we have a function called ``add`` which is defined like this:
28+
29+
.. code:: python
30+
31+
>>> def add(one: int, two: int) -> int:
32+
... return one + two
33+
34+
And we have two containers: ``IO(2)`` and ``IO(3)``.
35+
How can we easily get ``IO(5)`` in this case?
36+
37+
Luckily, ``IO`` defines :meth:`returns.io.IO.do` which can help us:
38+
39+
.. code:: python
40+
41+
>>> from returns.io import IO
42+
43+
>>> assert IO.do(
44+
... add(first, second)
45+
... for first in IO(2)
46+
... for second in IO(3)
47+
... ) == IO(5)
48+
49+
Notice, that you don't have two write any complicated code.
50+
Everything is pythonic and readable.
51+
52+
However, we still need to explain what ``for`` does here.
53+
It uses Python's ``__iter__`` method which returns an iterable
54+
with strictly a single raw value inside.
55+
56+
.. warning::
57+
58+
Please, don't use ``for x in container`` outside of do-notation.
59+
It does not make much sense.
60+
61+
Basically, for ``IO(2)`` it will return just ``2``.
62+
Then, ``IO.do`` wraps it into ``IO`` once again.
63+
64+
Errors
65+
~~~~~~
66+
67+
Containers like ``Result`` and ``IOResult`` can sometimes represent errors.
68+
In this case, do-notation expression will return the first found error.
69+
70+
For example:
71+
72+
.. code:: python
73+
74+
>>> from returns.result import Success, Failure, Result
75+
76+
>>> assert Result.do(
77+
... first + second
78+
... for first in Failure('a')
79+
... for second in Success(3)
80+
... ) == Failure('a')
81+
82+
This behavior is consistent with ``.map`` and other methods.
83+
84+
85+
Async containers
86+
----------------
87+
88+
We also support async containers like ``Future`` and ``FutureResult``.
89+
It works in a similar way as regular sync containers.
90+
But, they require ``async for`` expressions instead of regular ``for`` ones.
91+
And because of that - they cannot be used outsided of ``async def`` context.
92+
93+
Usage example:
94+
95+
.. code:: python
96+
97+
>>> import anyio
98+
>>> from returns.future import Future
99+
>>> from returns.io import IO
100+
101+
>>> async def main() -> None:
102+
... return await Future.do(
103+
... first + second
104+
... async for first in Future.from_value(1)
105+
... async for second in Future.from_value(2)
106+
... )
107+
108+
>>> assert anyio.run(main) == IO(3)
109+
110+
111+
FAQ
112+
---
113+
114+
Why don't we allow mixing different container types?
115+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
116+
117+
One might ask, why don't we allow mixing multiple container types
118+
in a single do-notation expression?
119+
120+
For example, this code will not what you expect:
121+
122+
.. code:: python
123+
124+
>>> from returns.result import Result, Success
125+
>>> from returns.io import IOResult, IOSuccess
126+
127+
>>> assert Result.do(
128+
... first + second
129+
... for first in Success(2)
130+
... for second in IOSuccess(3) # Notice the IO part here
131+
... ) == Success(5)
132+
133+
This code will raise a mypy error at ``for second in IOSuccess(3)`` part:
134+
135+
.. code::
136+
137+
Invalid type supplied in do-notation: expected "returns.result.Result[Any, Any]", got "returns.io.IOSuccess[builtins.int*]"
138+
139+
Notice, that the ``IO`` part is gone in the final result. This is not right.
140+
And we can't track this in any manner.
141+
So, we require all containers to have the same type.
142+
143+
The code above must be rewritten as:
144+
145+
.. code:: python
146+
147+
>>> from returns.result import Success
148+
>>> from returns.io import IOResult, IOSuccess
149+
150+
>>> assert IOResult.do(
151+
... first + second
152+
... for first in IOResult.from_result(Success(2))
153+
... for second in IOSuccess(3)
154+
... ) == IOSuccess(5)
155+
156+
Now, it is correct. ``IO`` part is safe, the final result is correct.
157+
And mypy is happy.
158+
159+
Why don't we allow ``if`` conditions in generator expressions?
160+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
161+
162+
At the moment, using ``if`` conditions inside generator expressions
163+
passed into ``.do`` method is not allowed. Why?
164+
165+
Because if the ``if`` condition will return ``False``,
166+
we will have an empty iterable and ``StopIteration`` will be thrown.
167+
168+
.. code:: python
169+
170+
>>> from returns.io import IO
171+
172+
>>> IO.do(
173+
... first + second
174+
... for first in IO(2)
175+
... for second in IO(3)
176+
... if second > 10
177+
... )
178+
Traceback (most recent call last):
179+
...
180+
StopIteration
181+
182+
It will raise:
183+
184+
.. code::
185+
186+
Using "if" conditions inside a generator is not allowed
187+
188+
Instead, use conditions and checks inside your logic, not inside your generator.
189+
190+
Why do we require a literal expression in do-notation?
191+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
192+
193+
This code will work in runtime, but will raise a mypy error:
194+
195+
.. code:: python
196+
197+
>>> from returns.result import Result, Success
198+
199+
>>> expr = (
200+
... first + second
201+
... for first in Success(2)
202+
... for second in Success(3)
203+
... )
204+
>>>
205+
>>> assert Result.do(expr) == Success(5)
206+
207+
It raises:
208+
209+
.. code::
210+
211+
Literal generator expression is required, not a variable or function call
212+
213+
This happens, because of mypy's plugin API.
214+
We need the whole expression to make sure it is correct.
215+
We cannot use variables and function calls in its place.
216+
217+
218+
Further reading
219+
---------------
220+
221+
- `Do notation in Haskell <https://en.wikibooks.org/wiki/Haskell/do_notation>`_

0 commit comments

Comments
 (0)