Skip to content

Commit a36273d

Browse files
committed
Refactors docs to be more IO specific
1 parent 687e37a commit a36273d

File tree

9 files changed

+247
-114
lines changed

9 files changed

+247
-114
lines changed

CHANGELOG.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,17 @@
33
We follow Semantic Versions since the `0.1.0` release.
44

55

6+
## WIP
7+
8+
### Features
9+
10+
- New types introduced: `FixableContainer` and `ValueUnwrapContainer`
11+
12+
### Misc
13+
14+
- Improved docs about `IO` and `Container` concept
15+
16+
617
## 0.7.0
718

819
### Features

docs/pages/container.rst

Lines changed: 68 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ Container: the concept
44
.. currentmodule:: returns.primitives.container
55

66
Container is a concept that allows you
7-
to write code without traditional error handling
7+
to write code around the existing wrapped values
88
while maintaining the execution context.
99

1010
We will show you its simple API of one attribute and several simple methods.
@@ -15,7 +15,7 @@ Basics
1515

1616
The main idea behind a container is that it wraps some internal state.
1717
That's what
18-
:py:attr:`_inner_value <returns.primitives.container.Container._inner_value>`
18+
:py:attr:`._inner_value <returns.primitives.container.Container._inner_value>`
1919
is used for.
2020

2121
And we have several functions
@@ -32,10 +32,61 @@ And we can see how this state is evolving during the execution.
3232
F4 --> F5["Container(SentNotificationId(992))"]
3333

3434

35+
Working with containers
36+
-----------------------
37+
38+
We use two methods to create new containers from the previous one.
39+
``bind`` and ``map``.
40+
41+
The difference is simple:
42+
43+
- ``map`` works with functions that return regular values
44+
- ``bind`` works with functions that return other containers of the same type
45+
46+
:func:`.bind <returns.primitives.container.Container.bind>`
47+
is used to literally bind two different containers together.
48+
49+
.. code:: python
50+
51+
from returns.result import Result, Success
52+
53+
def may_fail(user_id: int) -> Result[int, str]:
54+
...
55+
56+
result = Success(1).bind(may_fail)
57+
# => Will be equal to either Success[int] or Failure[str]
58+
59+
And we use :func:`.map <returns.primitives.container.Container.map>`
60+
to use containers with regular functions.
61+
62+
.. code:: python
63+
64+
from returns.result import Success
65+
66+
def double(state: int) -> int:
67+
return state * 2
68+
69+
result = Success(1).map(double)
70+
# => Will be equal to Success(2)
71+
72+
The same work with built-in functions as well:
73+
74+
.. code:: python
75+
76+
from returns.io import IO
77+
78+
IO('bytes').map(list)
79+
# => <IO: ['b', 'y', 't', 'e', 's']>
80+
81+
Note::
82+
83+
All containers support these methods.
84+
85+
3586
Railway oriented programming
3687
----------------------------
3788

38-
We use a concept of
89+
When talking about error handling we use a concept of
3990
`Railway oriented programming <https://fsharpforfunandprofit.com/rop/>`_.
4091
It mean that our code can go on two tracks:
4192

@@ -73,62 +124,16 @@ or we can rescue the situation.
73124
style F6 fill:red
74125
style F8 fill:red
75126

76-
77-
78-
Working with containers
79-
-----------------------
80-
81-
We use two methods to create new containers from the previous one.
82-
``bind`` and ``map``.
83-
84-
The difference is simple:
85-
86-
- ``map`` works with functions that return regular values
87-
- ``bind`` works with functions that return other containers
88-
89-
:func:`Container.bind <returns.primitives.container.Container.bind>`
90-
is used to literally bind two different containers together.
91-
92-
.. code:: python
93-
94-
from returns.result import Result, Success
95-
96-
def make_http_call(user_id: int) -> Result[int, str]:
97-
...
98-
99-
result = Success(1).bind(make_http_call)
100-
# => Will be equal to either Success[int] or Failure[str]
101-
102-
So, the rule is: whenever you have some impure functions,
103-
it should return a container type instead.
104-
105-
And we use :func:`Container.map <returns.primitives.container.Container.map>`
106-
to use containers with `pure functions <https://en.wikipedia.org/wiki/Pure_function>`_.
107-
108-
.. code:: python
109-
110-
from returns.result import Success
111-
112-
def double(state: int) -> int:
113-
return state * 2
114-
115-
result = Success(1).map(double)
116-
# => Will be equal to Success(2)
117-
118-
Note::
119-
120-
All containers support these methods.
121-
122127
Returning execution to the right track
123128
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
124129

125130
We also support two special methods to work with "failed"
126-
types like ``Failure`` and ``Nothing``:
131+
types like ``Failure``:
127132

128-
- :func:`Container.fix <returns.primitives.container.Container.fix>`
133+
- :func:`.fix <returns.primitives.container.FixableContainer.fix>`
129134
is the opposite of ``map`` method
130135
that works only when container is in failed state
131-
- :func:`Container.rescue <returns.primitives.container.Container.rescue>`
136+
- :func:`.rescue <returns.primitives.container.FixableContainer.rescue>`
132137
is the opposite of ``bind`` method
133138
that works only when container is in failed state
134139

@@ -145,20 +150,23 @@ during the pipeline execution:
145150
Failure(1).fix(double)
146151
# => Will be equal to Success(2.0)
147152
148-
``rescue`` can return any container type you want.
149-
It can also fix your flow and get on the successful track again:
153+
``rescue`` should return one of ``Success`` or ``Failure`` types.
154+
It can also rescue your flow and get on the successful track again:
150155

151156
.. code:: python
152157
153158
from returns.result import Result, Failure, Success
154159
155-
def fix(state: Exception) -> Result[int, Exception]:
160+
def tolerate_exception(state: Exception) -> Result[int, Exception]:
156161
if isinstance(state, ZeroDivisionError):
157162
return Success(0)
158163
return Failure(state)
159164
160-
Failure(ZeroDivisionError).rescue(fix)
161-
# => Will be equal to Success(0)
165+
Failure(ZeroDivisionError()).rescue(tolerate_exception)
166+
# => Success(0)
167+
168+
Failure(ValueError()).rescue(tolerate_exception)
169+
# => Failure(ValueError())
162170
163171
Note::
164172

@@ -171,9 +179,9 @@ Unwrapping values
171179
And we have two more functions to unwrap
172180
inner state of containers into a regular types:
173181

174-
- :func:`Container.value_or <returns.primitives.container.Container.value_or>`
182+
- :func:`.value_or <returns.primitives.container.ValueUnwrapContainer.value_or>`
175183
returns a value if it is possible, returns ``default_value`` otherwise
176-
- :func:`Container.unwrap <returns.primitives.container.Container.unwrap>`
184+
- :func:`.unwrap <returns.primitives.container.ValueUnwrapContainer.unwrap>`
177185
returns a value if it is possible, raises ``UnwrapFailedError`` otherwise
178186

179187
.. code:: python
@@ -195,7 +203,7 @@ inner state of containers into a regular types:
195203
The most user-friendly way to use ``unwrap`` method is with :ref:`pipeline`.
196204

197205
For failing containers you can
198-
use :func:`Container.failure <returns.primitives.container.Container.failure>`
206+
use :func:`.failure <returns.primitives.container.FixableContainer.failure>`
199207
to unwrap the failed state:
200208

201209
.. code:: python

docs/pages/io.rst

Lines changed: 56 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,9 @@ We can later use its result to notify users about their booking request:
3434
3535
notify_user_about_booking_result(is_successful) # works just fine!
3636
37+
Impure functions
38+
~~~~~~~~~~~~~~~~
39+
3740
But, imagine that our requirements had changed.
3841
And now we have to grab the number of already booked tickets
3942
from some other provider and fetch the maximum capacity from the database:
@@ -57,14 +60,18 @@ It will require to setup:
5760
- real database and tables
5861
- fixture data
5962
- ``requests`` mocks for different outcomes
63+
- and the whole Universe!
6064

6165
Our complexity has sky-rocketed!
6266
And the most annoying part is that all other functions
6367
that call ``can_book_seats`` now also have to do the same setup.
64-
It seams like ``IO`` is indelible mark.
68+
It seams like ``IO`` is indelible mark (some people also call it "effect").
6569

6670
And at some point it time we will start to mix pure and impure code together.
6771

72+
Separating two worlds
73+
~~~~~~~~~~~~~~~~~~~~~
74+
6875
Well, our :py:class:`IO <returns.io.IO>`
6976
mark is indeed indelible and should be respected.
7077

@@ -133,6 +140,7 @@ We can track it, we can fight it, we can design it better.
133140
By saying that, it is assumed that
134141
you have a functional core and imperative shell.
135142

143+
136144
impure
137145
------
138146

@@ -145,13 +153,60 @@ you with the existing impure things in Python:
145153
146154
name: IO[str] = impure(input)('What is your name?')
147155
156+
You can also decorate your own functions
157+
with ``@impure`` for better readability and clearness:
158+
159+
.. code:: python
160+
161+
import requests
162+
from returns.io import impure
163+
164+
@impure
165+
def get_user() -> 'User':
166+
return requests.get('https:...').json()
167+
148168
Limitations
149169
~~~~~~~~~~~
150170

151171
There's one limitation in typing
152172
that we are facing right now
153173
due to `mypy issue <https://github.com/python/mypy/issues/3157>`_.
154174

175+
176+
FAQ
177+
---
178+
179+
What is the difference between IO[T] and T?
180+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
181+
182+
What kind of input parameter should
183+
my function accept ``IO[T]`` or simple ``T``?
184+
185+
It really depends on your domain / context.
186+
If the value is pure, than use raw unwrapped values.
187+
If the value is fetched, input, received, selected, than use ``IO`` container.
188+
189+
Most web applications are just covered with ``IO``.
190+
191+
Why can't we unwrap values or use @pipeline with IO?
192+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
193+
194+
Our design decision was not let people unwrap ``IO`` containers,
195+
so it will indeed infect the whole call-stack with its effect.
196+
197+
Otherwise, people might hack the system
198+
in some dirty (from our point of view)
199+
but valid (from the python's point of view) ways.
200+
201+
Warning::
202+
203+
Of course, you can directly access
204+
the internal state of the IO with `._internal_state`,
205+
but your are considered to be a grown-up!
206+
207+
Use wemake-python-styleguide to restrict `._` access in your code.
208+
209+
155210
Further reading
156211
---------------
157212

docs/pages/unsafe.rst

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
unsafe
22
======
33

4-
Sometimes you really need to get the raw value.
4+
Sometimes you really need to get the raw value from ``IO`` container.
55
For example:
66

77
.. code:: python
@@ -12,8 +12,9 @@ For example:
1212
1313
In this case your web-framework will not render your user correctly.
1414
Since it does not expect it to be wrapped inside ``IO`` containers.
15+
And we obviously cannot ``map`` or ``bind`` this function.
1516

16-
What to do? Use ``unsafe_perform_io``:
17+
What to do? Use :func:`unsafe_perform_io <returns.unsafe.unsafe_perform_io>`:
1718

1819
.. code::
1920
@@ -32,6 +33,7 @@ to restrict imports from ``returns.unsafe`` expect the top-level modules.
3233
Inspired by Haskell's
3334
`unsafePerformIO <https://hackage.haskell.org/package/base-4.12.0.0/docs/System-IO-Unsafe.html#v:unsafePerformIO>`_
3435

36+
3537
API Reference
3638
-------------
3739

returns/functions.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,16 @@ def compose(first, second):
88
Works as: ``second . first``
99
You can read it as "second after first".
1010
11+
.. code:: python
12+
13+
from returns.functions import compose
14+
15+
logged_int = compose(int, print)('123')
16+
# => returns: 123
17+
# => prints: 123
18+
1119
We can only compose functions with one argument and one return.
20+
Type checked.
1221
"""
1322
return lambda argument: second(first(argument))
1423

@@ -17,6 +26,8 @@ def raise_exception(exception):
1726
"""
1827
Helper function to raise exceptions as a function.
1928
29+
It might be required as a compatibility tool for existing APIs.
30+
2031
That's how it can be used:
2132
2233
.. code:: python
@@ -25,6 +36,7 @@ def raise_exception(exception):
2536
2637
# Some operation result:
2738
user: Failure[UserDoesNotExistError]
39+
2840
# Here we unwrap internal exception and raise it:
2941
user.fix(raise_exception)
3042

0 commit comments

Comments
 (0)