File: do-notation.rst

package info (click to toggle)
python-returns 0.26.0-1
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid
  • size: 2,652 kB
  • sloc: python: 11,000; makefile: 18
file content (221 lines) | stat: -rw-r--r-- 5,660 bytes parent folder | download | duplicates (2)
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
.. _do-notation:

Do Notation
===========

.. note::

  Technical note: this feature requires :ref:`mypy plugin <mypy-plugins>`.

All containers can be easily composed
with functions that can take a single argument.

But, what if we need to compose two containers
with a function with two arguments?
That's not so easy.

Of course, we can use :ref:`curry` and ``.apply`` or some imperative code.
But, it is not very easy to write and read.

This is why multiple functional languages have a concept of "do-notation".
It allows you to write beautiful imperative code.


Regular containers
------------------

Let's say we have a function called ``add`` which is defined like this:

.. code:: python

  >>> def add(one: int, two: int) -> int:
  ...     return one + two

And we have two containers: ``IO(2)`` and ``IO(3)``.
How can we easily get ``IO(5)`` in this case?

Luckily, ``IO`` defines :meth:`~returns.io.IO.do` which can help us:

.. code:: python

  >>> from returns.io import IO

  >>> assert IO.do(
  ...     add(first, second)
  ...     for first in IO(2)
  ...     for second in IO(3)
  ... ) == IO(5)

Notice, that you don't have two write any complicated code.
Everything is pythonic and readable.

However, we still need to explain what ``for`` does here.
It uses Python's ``__iter__`` method which returns an iterable
with strictly a single raw value inside.

.. warning::

  Please, don't use ``for x in container`` outside of do-notation.
  It does not make much sense.

Basically, for ``IO(2)`` it will return just ``2``.
Then, ``IO.do`` wraps it into ``IO`` once again.

Errors
~~~~~~

Containers like ``Result`` and ``IOResult`` can sometimes represent errors.
In this case, do-notation expression will return the first found error.

For example:

.. code:: python

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

  >>> assert Result.do(
  ...     first + second
  ...     for first in Failure('a')
  ...     for second in Success(3)
  ... ) == Failure('a')

This behavior is consistent with ``.map`` and other methods.


Async containers
----------------

We also support async containers like ``Future`` and ``FutureResult``.
It works in a similar way as regular sync containers.
But, they require ``async for`` expressions instead of regular ``for`` ones.
And because of that - they cannot be used outside of ``async def`` context.

Usage example:

.. code:: python

  >>> import anyio
  >>> from returns.future import Future
  >>> from returns.io import IO

  >>> async def main() -> None:
  ...     return await Future.do(
  ...         first + second
  ...         async for first in Future.from_value(1)
  ...         async for second in Future.from_value(2)
  ...     )

  >>> assert anyio.run(main) == IO(3)


FAQ
---

Why don't we allow mixing different container types?
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

One might ask, why don't we allow mixing multiple container types
in a single do-notation expression?

For example, this code will not do what you expect:

.. code:: python

  >>> from returns.result import Result, Success
  >>> from returns.io import IOResult, IOSuccess

  >>> assert Result.do(
  ...     first + second
  ...     for first in Success(2)
  ...     for second in IOSuccess(3)  # Notice the IO part here
  ... ) == Success(5)

This code will raise a mypy error at ``for second in IOSuccess(3)`` part:

.. code::

  Invalid type supplied in do-notation: expected "returns.result.Result[Any, Any]", got "returns.io.IOSuccess[builtins.int*]"

Notice, that the ``IO`` part is gone in the final result. This is not right.
And we can't track this in any manner.
So, we require all containers to have the same type.

The code above must be rewritten as:

.. code:: python

  >>> from returns.result import Success
  >>> from returns.io import IOResult, IOSuccess

  >>> assert IOResult.do(
  ...     first + second
  ...     for first in IOResult.from_result(Success(2))
  ...     for second in IOSuccess(3)
  ... ) == IOSuccess(5)

Now, it is correct. ``IO`` part is safe, the final result is correct.
And mypy is happy.

Why don't we allow ``if`` conditions in generator expressions?
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

At the moment, using ``if`` conditions inside generator expressions
passed into ``.do`` method is not allowed. Why?

Because if the ``if`` condition will return ``False``,
we will have an empty iterable and ``StopIteration`` will be thrown.

.. code:: python

  >>> from returns.io import IO

  >>> IO.do(
  ...     first + second
  ...     for first in IO(2)
  ...     for second in IO(3)
  ...     if second > 10
  ... )
  Traceback (most recent call last):
    ...
  StopIteration

It will raise:

.. code::

  Using "if" conditions inside a generator is not allowed

Instead, use conditions and checks inside your logic, not inside your generator.

Why do we require a literal expression in do-notation?
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

This code will work in runtime, but will raise a mypy error:

.. code:: python

  >>> from returns.result import Result, Success

  >>> expr = (
  ...     first + second
  ...     for first in Success(2)
  ...     for second in Success(3)
  ... )
  >>>
  >>> assert Result.do(expr) == Success(5)

It raises:

.. code::

  Literal generator expression is required, not a variable or function call

This happens, because of mypy's plugin API.
We need the whole expression to make sure it is correct.
We cannot use variables and function calls in its place.


Further reading
---------------

- `Do notation in Haskell <https://en.wikibooks.org/wiki/Haskell/do_notation>`_