|
| 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