Skip to content

Commit d55b51c

Browse files
committed
Introduces lift, pipe, and box
1 parent a76e8e6 commit d55b51c

File tree

23 files changed

+451
-39
lines changed

23 files changed

+451
-39
lines changed

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -195,3 +195,6 @@ com_crashlytics_export_strings.xml
195195
crashlytics.properties
196196
crashlytics-build.properties
197197
fabric.properties
198+
199+
### Custom ###
200+
ex.py

CHANGELOG.md

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,26 @@
11
# Version history
22

3-
We follow Semantic Versions since the `0.1.0` release.
3+
We follow Semantic Versions since the `1.0.0` release.
4+
Versions before `1.0.0` are `0Ver`-based:
5+
incremental in minor, bugfixes only are patches.
6+
See (0Ver)[https://0ver.org/].
47

58

69
## 0.10.0 WIP
710

811
### Features
912

10-
- Now `bind` does not change the type of an error
11-
- Now `rescue` does not change the type of a value
12-
- Renames `map_failure` to `alt`
13-
- Adds `__hash__` magic methods to all containers
13+
- **Breaking**: `python>=3.7,<=3.7.2` are not supported anymore,
14+
because of a bug inside `typing` module
15+
- **Breaking**: Now `bind` does not change the type of an error
16+
- **Breaking**: Now `rescue` does not change the type of a value
17+
- **Breaking**: Renames `map_failure` to `alt`
18+
- Adds `lift()` function with the ability
19+
to lift function for direct container composition like:
20+
`a -> Container[b]` to `Container[a] -> Container[b]`
21+
- Adds `lift_io()` function to lift `a -> a` to `IO[a] -> IO[a]`
22+
- Adds `pipe()` function to `pipeline.py`
23+
- Adds `__hash__()` magic methods to all containers
1424

1525
### Bugfixes
1626

@@ -19,9 +29,10 @@ We follow Semantic Versions since the `0.1.0` release.
1929

2030
### Misc
2131

32+
- Massive docs rewrite
2233
- Updates `mypy` version
2334
- Updates `wemake-python-styleguide` and introduces `nitpick`
24-
- Updates `pytest-plugin-mypy`
35+
- Updates `pytest-plugin-mypy`, all tests now use `yml`
2536

2637

2738
## 0.9.0

README.md

Lines changed: 24 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -107,20 +107,24 @@ just to catch the expected exceptions.
107107

108108
Our code will become complex and unreadable with all this mess!
109109

110-
### Pipeline example
110+
### Pipe example
111111

112112
```python
113113
import requests
114-
from returns.result import Result, pipeline, safe
114+
from returns.result import Result, safe
115+
from returns.pipeline import pipe
116+
from returns.functions import lift
115117

116118
class FetchUserProfile(object):
117119
"""Single responsibility callable object that fetches user profile."""
118120

119-
@pipeline
120121
def __call__(self, user_id: int) -> Result['UserProfile', Exception]:
121-
"""Fetches UserProfile dict from foreign API."""
122-
response = self._make_request(user_id).unwrap()
123-
return self._parse_json(response)
122+
"""Fetches `UserProfile` TypedDict from foreign API."""
123+
return pipe(
124+
user_id,
125+
self._make_request,
126+
lift(self._parse_json),
127+
)
124128

125129
@safe
126130
def _make_request(self, user_id: int) -> requests.Response:
@@ -145,20 +149,14 @@ decorator.
145149
It will return [Success[Response] or Failure[Exception]](https://returns.readthedocs.io/en/latest/pages/result.html).
146150
And will never throw this exception at us.
147151

148-
When we will need raw value, we can use `.unwrap()` method to get it.
149-
If the result is `Failure[Exception]`
150-
we will actually raise an exception at this point.
151-
But it is safe to use `.unwrap()` inside
152-
[@pipeline](https://returns.readthedocs.io/en/latest/pages/functions.html#returns.functions.pipeline)
153-
functions.
154-
Because it will catch this exception
155-
and wrap it inside a new `Failure[Exception]`!
156-
157152
And we can clearly see all result patterns
158153
that might happen in this particular case:
159154
- `Success[UserProfile]`
160155
- `Failure[Exception]`
161156

157+
For more complex cases there's a [@pipeline](https://returns.readthedocs.io/en/latest/pages/functions.html#returns.functions.pipeline)
158+
decorator to help you with the composition.
159+
162160
And we can work with each of them precisely.
163161
It is a good practice to create `Enum` classes or `Union` sum type
164162
with all the possible errors.
@@ -188,16 +186,21 @@ Let's refactor it to make our
188186
```python
189187
import requests
190188
from returns.io import IO, impure
191-
from returns.result import Result, pipeline, safe
189+
from returns.result import Result, safe
190+
from returns.pipeline import pipe
191+
from returns.functions import lift, lift_io
192192

193193
class FetchUserProfile(object):
194194
"""Single responsibility callable object that fetches user profile."""
195195

196-
@pipeline
197196
def __call__(self, user_id: int) -> IO[Result['UserProfile', Exception]]:
198-
"""Fetches UserProfile dict from foreign API."""
199-
return self._make_request(user_id).map(
200-
lambda response: self._parse_json(response.unwrap())
197+
"""Fetches `UserProfile` TypedDict from foreign API."""
198+
return pipe(
199+
user_id,
200+
self._make_request,
201+
# lift: def (Result) -> Result
202+
# lift_io: def (IO[Result]) -> IO[Result]
203+
lift(box(self._parse_json), IO),
201204
)
202205

203206
@impure
@@ -208,7 +211,7 @@ class FetchUserProfile(object):
208211
return response
209212

210213
@safe
211-
def _parse_json(self,response: requests.Response) -> 'UserProfile':
214+
def _parse_json(self, response: requests.Response) -> 'UserProfile':
212215
return response.json()
213216
```
214217

poetry.lock

Lines changed: 15 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,3 +66,4 @@ sphinx-typlog-theme = "^0.7.1"
6666
doc8 = "^0.8.0"
6767
m2r = "^0.2.1"
6868
tomlkit = "^0.5.5"
69+
flake8-pyi = "^19.3"

returns/converters.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ def maybe_to_result(
2323
"""Converts ``Maybe`` container to ``Result`` container."""
2424
inner_value = maybe_container.value_or(None)
2525
if inner_value is not None:
26-
return Success(inner_value) # type: ignore
26+
return Success(inner_value)
2727
return Failure(inner_value)
2828

2929

returns/functions.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,9 @@
22

33
from typing import Callable, NoReturn, TypeVar
44

5-
# Just aliases:
5+
from returns.generated.box import _box as box # noqa: F401, WPS436
6+
7+
# Aliases:
68
_FirstType = TypeVar('_FirstType')
79
_SecondType = TypeVar('_SecondType')
810
_ThirdType = TypeVar('_ThirdType')
@@ -15,7 +17,7 @@ def compose(
1517
"""
1618
Allows function composition.
1719
18-
Works as: ``second . first``
20+
Works as: ``second . first`` or ``first() |> second()``.
1921
You can read it as "second after first".
2022
2123
.. code:: python

returns/generated/__init__.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
# -*- coding: utf-8 -*-
2+
3+
"""
4+
This package contains code that was "generated" via metaprogramming.
5+
6+
This happens, because python is not flexible enough to do most common
7+
tasks in typed functional programming.
8+
9+
Policy:
10+
11+
1. We store implementations in regular ``.py`` files.
12+
2. We store generated type annotations in ``.pyi`` files.
13+
3. We re-export these functions into regular modules as public values.
14+
15+
Please, do not touch this code unless you know what you are doing.
16+
"""

returns/generated/box.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
# -*- coding: utf-8 -*-
2+
3+
4+
def _box(function):
5+
"""
6+
Boxes function's input parameter from a regular value to a container.
7+
8+
In other words, it modifies the function
9+
signature from: ``a -> Container[b]`` to: ``Container[a] -> Container[b]``
10+
11+
This is how it should be used:
12+
13+
.. code:: python
14+
15+
>>> from returns.functions import box
16+
>>> from returns.maybe import Maybe, Some
17+
>>> def example(argument: int) -> Maybe[int]:
18+
... return Some(argument + 1)
19+
...
20+
>>> box(example)(Some(1)) == Some(2)
21+
True
22+
23+
"""
24+
return lambda container: container.bind(function)

returns/generated/box.pyi

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
# -*- coding: utf-8 -*-
2+
3+
from typing import Callable, TypeVar, overload
4+
5+
from returns.io import IO
6+
from returns.maybe import Maybe
7+
from returns.result import Result
8+
9+
_ValueType = TypeVar('_ValueType')
10+
_ErrorType = TypeVar('_ErrorType')
11+
_NewValueType = TypeVar('_NewValueType')
12+
13+
14+
# Box:
15+
16+
@overload
17+
def _box(
18+
function: Callable[[_ValueType], Maybe[_NewValueType]],
19+
) -> Callable[[Maybe[_ValueType]], Maybe[_NewValueType]]:
20+
...
21+
22+
23+
@overload
24+
def _box(
25+
function: Callable[[_ValueType], IO[_NewValueType]],
26+
) -> Callable[[IO[_ValueType]], IO[_NewValueType]]:
27+
...
28+
29+
30+
@overload
31+
def _box(
32+
function: Callable[[_ValueType], Result[_NewValueType, _ErrorType]],
33+
) -> Callable[
34+
[Result[_ValueType, _ErrorType]], Result[_NewValueType, _ErrorType],
35+
]:
36+
...

returns/generated/pipe.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
# -*- coding: utf-8 -*-
2+
3+
from functools import reduce
4+
5+
from returns.functions import compose
6+
7+
8+
def _pipe(initial, *functions):
9+
"""
10+
Allows to compose a value and up to 7 functions that use this value.
11+
12+
Each next function uses the previos result as an input parameter.
13+
Here's how it should be used:
14+
15+
.. code:: python
16+
17+
>>> from returns.pipeline import pipe
18+
19+
# => executes: str(float(int('1')))
20+
>>> pipe('1', int, float, str)
21+
'1.0'
22+
23+
See also:
24+
https://stackoverflow.com/a/41585450/4842742
25+
https://github.com/gcanti/fp-ts/blob/master/src/pipeable.ts
26+
27+
"""
28+
return reduce(compose, functions)(initial)

0 commit comments

Comments
 (0)