Skip to content

Commit edb8ae2

Browse files
committed
Working on docs
1 parent 49653ab commit edb8ae2

File tree

3 files changed

+100
-58
lines changed

3 files changed

+100
-58
lines changed

README.md

Lines changed: 29 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,9 @@ Make your functions return something meaningful, typed, and safe!
1414
## Features
1515

1616
- Provides a bunch of primitives to write declarative business logic
17+
- Enforces [Railway Oriented Programming](https://fsharpforfunandprofit.com/rop/)
1718
- Fully typed with annotations and checked with `mypy`,
18-
allowing you to write type-safe code as well
19+
allowing you to write type-safe code as well, [PEP561 compatible](https://www.python.org/dev/peps/pep-0561/)
1920
- Pythonic and pleasant to write and to read (!)
2021

2122

@@ -28,6 +29,7 @@ pip install returns
2829

2930
Make sure you know how to get started, [checkout our docs](https://returns.readthedocs.io/en/latest/)!
3031

32+
3133
## Why?
3234

3335
Consider this code that you can find in **any** `python` project.
@@ -37,52 +39,50 @@ Consider this code that you can find in **any** `python` project.
3739
```python
3840
import requests
3941

40-
4142
def fetch_user_profile(user_id: int) -> 'UserProfile':
4243
"""Fetches UserProfile dict from foreign API."""
4344
response = requests.get('/api/users/{0}'.format(user_id))
4445
response.raise_for_status()
4546
return response.json()
4647
```
4748

48-
Seems legit, does not it?
49-
It also seems like a pretty straight forward code to test.
49+
Seems legit, does not it?
50+
It also seems like a pretty straight forward code to test.
5051
All you need is to mock `requests.get` to return the structure you need.
5152

52-
But, there are hidden problems in this tiny code sample
53+
But, there are hidden problems in this tiny code sample
5354
that are almost impossible to spot at the first glance.
5455

5556
### Hidden problems
5657

57-
58-
The same exact code, but with the problems explained.
58+
Let's have a look at the exact same code,
59+
but with the all hidden problems explained.
5960

6061
```python
6162
import requests
6263

63-
6464
def fetch_user_profile(user_id: int) -> 'UserProfile':
6565
"""Fetches UserProfile dict from foreign API."""
6666
response = requests.get('/api/users/{0}'.format(user_id))
67-
68-
# What if we try to find user that does not exist?
67+
68+
# What if we try to find user that does not exist?
6969
# Or network will go down? Or the server will return 500?
7070
# In this case the next line will fail with an exception.
71-
# We need to handle all possible errors in this function
71+
# We need to handle all possible errors in this function
7272
# and do not return corrupt data to consumers.
7373
response.raise_for_status()
7474

75-
# What if we have received invalid JSON?
75+
# What if we have received invalid JSON?
7676
# Next line will raise an exception!
7777
return response.json()
7878
```
7979

80-
Now, all (probably all?) problems are clear.
81-
How can we be sure that this function will be safe
80+
Now, all (probably all?) problems are clear.
81+
How can we be sure that this function will be safe
8282
to use inside our complex business logic?
8383

84-
We really can not be sure!
85-
We will have to create **lots** of `try` and `except` cases
84+
We really can not be sure!
85+
We will have to create **lots** of `try` and `except` cases
8686
just to catch the expected exceptions.
8787

8888
Our code will become complex and unreadable with all this mess!
@@ -96,26 +96,25 @@ import requests
9696
from returns.functions import pipeline, safe
9797
from returns.result import Result
9898

99-
10099
class FetchUserProfile(object):
101-
"""Single responibility callable object that fetches user profiles."""
102-
103-
#: You can later use dependency injection to replace `requests`
104-
#: with any other http library you want.
100+
"""Single responsibility callable object that fetches user profile."""
101+
102+
#: You can later use dependency injection to replace `requests`
103+
#: with any other http library (or even a custom service).
105104
_http = requests
106-
105+
107106
@pipeline
108107
def __call__(self, user_id: int) -> Result['UserProfile', Exception]:
109108
"""Fetches UserProfile dict from foreign API."""
110109
response = self._make_request(user_id).unwrap()
111110
return self._parse_json(response)
112-
111+
113112
@safe
114113
def _make_request(self, user_id: int) -> requests.Response:
115114
response = self._http.get('/api/users/{0}'.format(user_id))
116115
response.raise_for_status()
117116
return response
118-
117+
119118
@safe
120119
def _parse_json(self, response: requests.Response) -> 'UserProfile':
121120
return response.json()
@@ -124,18 +123,18 @@ class FetchUserProfile(object):
124123
Now we have a clean and a safe way to express our business need.
125124
We start from making a request, that might fail at any moment.
126125

127-
Now, instead of returning a regular value
126+
Now, instead of returning a regular value
128127
it returns a wrapped value inside a special container
129128
thanks to the
130129
[@safe](https://returns.readthedocs.io/en/latest/pages/functions.html#returns.functions.safe)
131130
decorator.
132131

133-
It will return [Success[Response] or Failure[Exception]](https://returns.readthedocs.io/en/latest/pages/result.html).
132+
It will return [Success[Response] or Failure[Exception]](https://returns.readthedocs.io/en/latest/pages/result.html).
134133
And will never throw this exception at us.
135134

136135
When we will need raw value, we can use `.unwrap()` method to get it.
137136
If the result is `Failure[Exception]` we will actually raise an exception at this point.
138-
But it is safe to use `.unwrap()` inside [@pipeline](https://returns.readthedocs.io/en/latest/pages/functions.html#returns.functions.pipeline)
137+
But it is safe to use `.unwrap()` inside [@pipeline](https://returns.readthedocs.io/en/latest/pages/functions.html#returns.functions.pipeline)
139138
functions.
140139
Because it will catch this exception and wrap it inside a new `Failure[Exception]`!
141140

@@ -144,6 +143,9 @@ And we can clearly see all result patterns that might happen in this particular
144143
- `Failure[HttpException]`
145144
- `Failure[JsonDecodeException]`
146145

146+
And we can work with each of them precisely.
147+
148+
What more? [Go to the docs!](https://returns.readthedocs.io)
147149

148150
## License
149151

docs/pages/container.rst

Lines changed: 70 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,9 @@ while maintaining the execution context.
99

1010
We will show you its simple API of one attribute and several simple methods.
1111

12-
Internals
13-
---------
12+
13+
Basics
14+
------
1415

1516
The main idea behind a container is that it wraps some internal state.
1617
That's what
@@ -25,21 +26,65 @@ And we can see how this state is evolving during the execution.
2526
:caption: State evolution.
2627

2728
graph LR
28-
F1["State()"] --> F2["State(UserId(1))"]
29-
F2 --> F3["State(UserAccount(156))"]
30-
F3 --> F4["State(FailedLoginAttempt(1))"]
31-
F4 --> F5["State(SentNotificationId(992))"]
29+
F1["Container(Initial)"] --> F2["Container(UserId(1))"]
30+
F2 --> F3["Container(UserAccount(156))"]
31+
F3 --> F4["Container(FailedLoginAttempt(1))"]
32+
F4 --> F5["Container(SentNotificationId(992))"]
33+
34+
35+
Railway oriented programming
36+
----------------------------
37+
38+
We use a concept of
39+
`Railway oriented programming <https://fsharpforfunandprofit.com/rop/>`_.
40+
It mean that our code can go on two tracks:
41+
42+
1. Successful one: where everything goes perfectly: HTTP requests work,
43+
database is always serving us data, parsing values does not failed
44+
2. Failed one: where something went wrong
45+
46+
We can switch from track to track: we can fail something
47+
or we can rescue the situation.
48+
49+
.. mermaid::
50+
:caption: Railway oriented programming.
51+
52+
graph LR
53+
S1 --> S3
54+
S3 --> S5
55+
S5 --> S7
56+
57+
F2 --> F4
58+
F4 --> F6
59+
F6 --> F8
3260

33-
Creating new containers
34-
~~~~~~~~~~~~~~~~~~~~~~~
61+
S1 -- Fail --> F2
62+
F2 -- Fix --> S3
63+
S3 -- Fail --> F4
64+
S5 -- Fail --> F6
65+
F6 -- Rescue --> S7
66+
67+
style S1 fill:green
68+
style S3 fill:green
69+
style S5 fill:green
70+
style S7 fill:green
71+
style F2 fill:red
72+
style F4 fill:red
73+
style F6 fill:red
74+
style F8 fill:red
75+
76+
77+
78+
Working with containers
79+
-----------------------
3580

3681
We use two methods to create new containers from the previous one.
3782
``bind`` and ``map``.
3883

3984
The difference is simple:
4085

4186
- ``map`` works with functions that return regular values
42-
- ``bind`` works with functions that return containers
87+
- ``bind`` works with functions that return other containers
4388

4489
:func:`Container.bind <returns.primitives.container.Container.bind>`
4590
is used to literally bind two different containers together.
@@ -70,8 +115,8 @@ to use containers with `pure functions <https://en.wikipedia.org/wiki/Pure_funct
70115
result = Success(1).map(double)
71116
# => Will be equal to Success(2)
72117
73-
Reverse operations
74-
~~~~~~~~~~~~~~~~~~
118+
Returning execution to the right track
119+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
75120

76121
We also support two special methods to work with "failed"
77122
types like ``Failure`` and ``Nothing``:
@@ -138,7 +183,7 @@ inner state of containers into a regular types:
138183
Failure(1).unwrap()
139184
# => Traceback (most recent call last): UnwrapFailedError
140185
141-
The most user-friendly way to use ``unwrap`` method is with :ref:`do-notation`.
186+
The most user-friendly way to use ``unwrap`` method is with :ref:`pipeline`.
142187

143188
For failing containers you can
144189
use :func:`Container.failure <returns.primitives.container.Container.failure>`
@@ -155,6 +200,7 @@ to unwrap the failed state:
155200
Be careful, since this method will raise an exception
156201
when you try to ``failure`` a successful container.
157202

203+
158204
Immutability
159205
------------
160206

@@ -167,32 +213,25 @@ since we are using ``__slots__`` for better performance and strictness.
167213

168214
Well, nothing is **really** immutable in python, but you were warned.
169215

170-
Using lambda functions
171-
----------------------
172216

173-
Please, do not use ``lambda`` functions in ``python``. Why?
174-
Because all ``lambda`` functions arguments are typed as ``Any``.
175-
This way you won't have any practical typing features
176-
from ``map`` and ``bind`` methods.
217+
Type safety
218+
-----------
177219

178-
So, instead of:
220+
We try to make our containers optionally type safe.
179221

180-
.. code:: python
181-
182-
some_container.map(lambda x: x + 2) #: Callable[[Any], Any]
183-
184-
Write:
185-
186-
.. code:: python
222+
What does it mean?
187223

188-
from functools import partial
224+
1. It is still good old ``python``, do whatever you want without ``mypy``
225+
2. If you are using ``mypy`` you will be notified about type violations
189226

190-
def increment(addition: int, number: int) -> int:
191-
return number + addition
227+
We also ship `PEP561 <https://www.python.org/dev/peps/pep-0561/>`_
228+
compatible ``.pyi`` files together with the source code.
229+
In this case these types will be available to users
230+
when they install our application.
192231

193-
some_container.map(partial(increment, 2)) # functools.partial[builtins.int]
232+
However, this is still good old ``python`` type system,
233+
and it has its drawbacks.
194234

195-
This way your code will be type-safe from errors.
196235

197236
API Reference
198237
-------------

docs/pages/functions.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ and :class:`Some <returns.maybe.Some>`.
2525
is_successful(Nothing) or is_successful(Failure('text'))
2626
# => False
2727
28+
.. _pipeline:
2829

2930
pipeline
3031
--------

0 commit comments

Comments
 (0)