Skip to content

Commit 1f50586

Browse files
committed
WTF is going on with mypy?!
1 parent f0c4981 commit 1f50586

File tree

10 files changed

+283
-99
lines changed

10 files changed

+283
-99
lines changed

README.md

Lines changed: 82 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ Make sure you know how to get started, [check out our docs](https://returns.read
5252
- [Maybe container](#maybe-container) that allows you to write `None`-free code
5353
- [RequiresContext container](#requirescontext-container) that allows you to use typed functional dependency injection
5454
- [Result container](#result-container) that let's you to get rid of exceptions
55-
- [IO marker](#io-marker) that marks all impure operations and structures them
55+
- [IO marker](#io-marker) and [IOResult](#troublesome-io) that marks all impure operations and structures them
5656

5757

5858
## Maybe container
@@ -62,7 +62,7 @@ Make sure you know how to get started, [check out our docs](https://returns.read
6262
So, what can we do to check for `None` in our programs?
6363
You can use builtin [Optional](https://mypy.readthedocs.io/en/stable/kinds_of_types.html#optional-types-and-the-none-type) type
6464
and write a lot of `if some is not None:` conditions.
65-
But, having them here and there makes your code unreadable.
65+
But, **having `null` checks here and there makes your code unreadable**.
6666

6767
```python
6868
user: Optional[User]
@@ -167,7 +167,7 @@ Large code bases will struggle a lot from this change.
167167

168168
Ok, you can directly use `django.settings` (or similar)
169169
in your `_award_points_for_letters` function.
170-
And ruin your pure logic with framework specific details. That's ugly!
170+
And **ruin your pure logic with framework specific details**. That's ugly!
171171

172172
Or you can use `RequiresContext` container. Let's see how our code changes:
173173

@@ -281,6 +281,7 @@ def fetch_user_profile(user_id: int) -> Result['UserProfile', Exception]:
281281

282282
@safe
283283
def _make_request(user_id: int) -> requests.Response:
284+
# TODO: we are not yet done with this example, read more about `IO`:
284285
response = requests.get('/api/users/{0}'.format(user_id))
285286
response.raise_for_status()
286287
return response
@@ -291,43 +292,37 @@ def _parse_json(response: requests.Response) -> 'UserProfile':
291292
```
292293

293294
Now we have a clean and a safe and declarative way
294-
to express our business need.
295-
We start from making a request, that might fail at any moment.
296-
Then parsing the response if the request was successful.
297-
And then return the result.
298-
It all happens smoothly due to [pipe](https://returns.readthedocs.io/en/latest/pages/pipeline.html#pipe) function.
295+
to express our business needs:
299296

300-
We also use [box](https://returns.readthedocs.io/en/latest/pages/functions.html#box) for handy composition.
297+
- We start from making a request, that might fail at any moment,
298+
- Then parsing the response if the request was successful,
299+
- And then return the result.
301300

302-
Now, instead of returning a regular value
303-
it returns a wrapped value inside a special container
301+
Now, instead of returning regular values
302+
we return values wrapped inside a special container
304303
thanks to the
305304
[@safe](https://returns.readthedocs.io/en/latest/pages/result.html#safe)
306-
decorator.
305+
decorator. It will return [Success[YourType] or Failure[Exception]](https://returns.readthedocs.io/en/latest/pages/result.html).
306+
And will never throw exception at us!
307307

308-
It will return [Success[Response] or Failure[Exception]](https://returns.readthedocs.io/en/latest/pages/result.html).
309-
And will never throw this exception at us.
308+
We also use [pipe](https://returns.readthedocs.io/en/latest/pages/pipeline.html#pipe)
309+
and [bind](https://returns.readthedocs.io/en/latest/pages/pointfree.html#bind)
310+
functions for handy and declarative composition.
310311

311-
And we can clearly see all result patterns
312-
that might happen in this particular case:
312+
This way we can be sure that our code won't break in
313+
random places due to some implicit exception.
314+
Now we control all parts and are prepared for the explicit errors.
313315

314-
- `Success[UserProfile]`
315-
- `Failure[Exception]`
316-
317-
For more complex cases there's a [@pipeline](https://returns.readthedocs.io/en/latest/pages/functions.html#returns.functions.pipeline)
318-
decorator to help you with the composition.
319-
320-
And we can work with each of them precisely.
321-
It is a good practice to create `Enum` classes or `Union` sum type
322-
with all the possible errors.
316+
We are not yet done with this example,
317+
let's continue to improve it in the next chapter.
323318

324319

325320
## IO marker
326321

327-
But is that all we can improve?
328-
Let's look at `FetchUserProfile` from another angle.
329-
All its methods look like regular ones:
330-
it is impossible to tell whether they are pure or impure from the first sight.
322+
Let's look at our example from another angle.
323+
All its functions look like regular ones:
324+
it is impossible to tell whether they are [pure](https://en.wikipedia.org/wiki/Pure_function)
325+
or impure from the first sight.
331326

332327
It leads to a very important consequence:
333328
*we start to mix pure and impure code together*.
@@ -338,10 +333,59 @@ we suffer really bad when testing or reusing it.
338333
Almost everything should be pure by default.
339334
And we should explicitly mark impure parts of the program.
340335

336+
That's why we have created `IO` marker
337+
to mark impure functions that never fail.
338+
339+
These impure functions use `random`, current datetime, environment, or console:
340+
341+
```python
342+
import random
343+
import datetime as dt
344+
345+
from returns.io import IO
346+
347+
def get_random_number() -> IO[int]: # or use `@impure` decorator
348+
return IO(random.randint(1, 10)) # isn't pure, because random
349+
350+
now: Callable[[], IO[dt.datetime]] = impure(dt.datetime.now)
351+
352+
@impure
353+
def return_and_show_next_number(previous: int) -> int:
354+
next_number = previous + 1
355+
print(next_number) # isn't pure, because does IO
356+
return next_number
357+
```
358+
359+
Now we can clearly see which functions are pure and which ones are impure.
360+
This helps us a lot in building large applications, unit testing you code,
361+
and composing bussiness logic together.
362+
363+
### Troublesome IO
364+
365+
As it was already said, we use `IO` when we handle functions that do not fail.
366+
367+
What if our function can fail and is impure?
368+
Like `requests.get()` we had earlier in your example.
369+
370+
Then we have to use `IOResult` instead of a regular `Result`.
371+
Let's find the difference:
372+
373+
- Our `_parse_json` function always return
374+
the same result (hopefully) for the same input:
375+
you can either parse valid `json` or fail on invalid one.
376+
That's why we return pure `Result`
377+
- Our `_make_request` function is impure and can fail.
378+
Try to send two similar requests with and without internet connection.
379+
The result will be different for the same input.
380+
That's why we must use `IOResult` here
381+
382+
So, in order to fulfill our requirement and separate pure code from impure one,
383+
we have to refactor our example.
384+
341385
### Explicit IO
342386

343-
Let's refactor it to make our
344-
[IO](https://returns.readthedocs.io/en/latest/pages/io.html) explicit!
387+
Let's make our [IO](https://returns.readthedocs.io/en/latest/pages/io.html)
388+
explicit!
345389

346390
```python
347391
import requests
@@ -372,12 +416,14 @@ def _parse_json(response: requests.Response) -> 'UserProfile':
372416
return response.json()
373417
```
374418

375-
Now we have explicit markers where the `IO` did happen
376-
and these markers cannot be removed.
419+
And latter we can [unsafe_perform_io](https://returns.readthedocs.io/en/latest/pages/io.html#unsafe-perform-io)
420+
somewhere at the top level of our program to get the pure value.
421+
422+
As a result of this refactoring session, we know everything about our code:
377423

378-
Whenever we access `FetchUserProfile` we now know
379-
that it does `IO` and might fail.
380-
So, we act accordingly!
424+
- Which parts can fail,
425+
- Which parts are impure,
426+
- How to compose them in a smart manner.
381427

382428

383429
## More!

docs/conf.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ def _get_project_meta():
7676

7777
# Set `typing.TYPE_CHECKING` to `True`:
7878
# https://pypi.org/project/sphinx-autodoc-typehints/
79-
set_type_checking_flag = False
79+
set_type_checking_flag = True
8080

8181
# Add any paths that contain templates here, relative to this directory.
8282
templates_path = ['_templates']

docs/pages/io.rst

Lines changed: 45 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,35 @@
11
IO
22
==
33

4-
``IO`` is ugly.
4+
Mathematicians dream in pure functions.
5+
Each of them only relies on its arguments
6+
and always produces the same result for the same input.
7+
8+
That's not how useful program work.
9+
We need to rely on the environment and we need to do side effects.
10+
11+
Furthermore, there are several types of ``IO`` in our programs:
12+
13+
- Some ``IO`` never fails, like:
14+
getting current date and time, random number, or OS name
15+
- Some ``IO`` might fail, like:
16+
sending network requests, accessing filesystem, or database
17+
18+
There's a solution.
519

6-
Why? Let me illustrate it with the example.
720

821
IO marker
922
---------
1023

24+
We can use a simple class :class:`returns.io.IO`
25+
to mark impure parts of the program that do not fail.
26+
27+
28+
29+
30+
IOResult
31+
--------
32+
1133
Imagine we have this beautiful pure function:
1234

1335
.. code:: python
@@ -203,26 +225,6 @@ if :ref:`decorator_plugin <type-safety>` is used.
203225
This happens due to `mypy issue <https://github.com/python/mypy/issues/3157>`_.
204226

205227

206-
Laziness
207-
--------
208-
209-
Please, note that our ``IO`` implementation is not lazy.
210-
This way when you mark something as ``@impure`` it will work as previously.
211-
The only thing that changes is type.
212-
213-
Instead we offer to use :ref:`unsafe_perform_io`
214-
to work with ``IO`` and simulate laziness.
215-
216-
But, you can always make your ``IO`` lazy:
217-
218-
.. code:: python
219-
220-
>>> from returns.io import IO
221-
>>> lazy = lambda: IO(1)
222-
>>> str(lazy())
223-
'<IO: 1>'
224-
225-
226228
io_squash
227229
---------
228230

@@ -303,6 +305,27 @@ Inspired by Haskell's
303305
FAQ
304306
---
305307

308+
Why aren't IO lazy?
309+
~~~~~~~~~~~~~~~~~~~
310+
311+
Please, note that our ``IO`` implementation is not lazy by design.
312+
This way when you mark something as ``@impure`` it will work as previously.
313+
The only thing that changes is the return type.
314+
315+
Instead we offer to use :ref:`unsafe_perform_io`
316+
to work with ``IO`` and simulate laziness.
317+
318+
But, you can always make your ``IO`` lazy:
319+
320+
.. code:: python
321+
322+
>>> from returns.io import IO
323+
>>> lazy = lambda: IO(1)
324+
>>> str(lazy())
325+
'<IO: 1>'
326+
327+
We have decided that it would be better and more familiar for Python devs.
328+
306329
What is the difference between IO[T] and T?
307330
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
308331

0 commit comments

Comments
 (0)