Skip to content

Commit cf36f26

Browse files
authored
WIP: adds Maybe monad (#89)
* WIP: adds Maybe monad * Maybe monad * Style fix
1 parent 92eda55 commit cf36f26

29 files changed

+1008
-117
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ We follow Semantic Versions since the `0.1.0` release.
77

88
### Features
99

10+
- Reintroduces the `Maybe` monad, typed!
1011
- Adds `mypy` plugin to type decorators
1112
- Complete rewrite of `Result` types
1213
- Partial API change, now `Success` and `Failure` are not types, but functions

CONTRIBUTING.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,22 @@ flake8 returns tests docs
4747

4848
These steps are mandatory during the CI.
4949

50+
### Fixing pytest coverage issue
51+
52+
Coverage does not work well with `pytest-mypy-plugin`,
53+
that's why we have two phases of `pytest` run.
54+
55+
If you accidentally mess things up
56+
and see `INTERNALERROR> coverage.misc.CoverageException` in your log,
57+
do:
58+
59+
```bash
60+
rm .coverage*
61+
rm -rf .pytest_cache htmlcov
62+
```
63+
64+
And it should solve it.
65+
Then use correct test commands.
5066

5167
## Type checks
5268

README.md

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ Make sure you know how to get started, [check out our docs](https://returns.read
4040

4141
- [Result container](#result-container) that let's you to get rid of exceptions
4242
- [IO marker](#io-marker) that marks all impure operations and structures them
43+
- [Maybe container](#maybe-container) that allows you to write `None`-free code
4344

4445

4546
## Result container
@@ -215,6 +216,38 @@ Whenever we access `FetchUserProfile` we now know
215216
that it does `IO` and might fail.
216217
So, we act accordingly!
217218

219+
220+
## Maybe container
221+
222+
Have you ever since code with a lot of `if some is not None` conditions?
223+
It really bloats your source code and makes it unreadable.
224+
225+
But, having `None` in your source code is even worth.
226+
Actually, `None` is called the [worth mistake in the history of Computer Science](https://www.infoq.com/presentations/Null-References-The-Billion-Dollar-Mistake-Tony-Hoare/).
227+
228+
So, what to do? Use `Maybe` container!
229+
It consists of `Some(...)` and `Nothing` types,
230+
representing existing state and `None` state respectively.
231+
232+
```python
233+
from typing import Optional
234+
from returns.maybe import Maybe
235+
236+
def bad_function() -> Optional[int]:
237+
...
238+
239+
maybe_result: Maybe[float] = Maybe.new(
240+
bad_function(),
241+
).map(
242+
lambda number: number / 2
243+
)
244+
# => Maybe will return Some(float) only if there's a non-None value
245+
# Otherwise, will return Nothing
246+
```
247+
248+
Forget about `None`-related errors forever!
249+
250+
218251
## More!
219252

220253
Want more? [Go to the docs!](https://returns.readthedocs.io)

docs/index.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ Contents
1818

1919
pages/container.rst
2020
pages/result.rst
21+
pages/maybe.rst
2122
pages/io.rst
2223
pages/unsafe.rst
2324
pages/functions.rst

docs/pages/maybe.rst

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
Maybe
2+
=====
3+
4+
The ``Maybe`` container is used when a series of computations
5+
could return ``None`` at any point.
6+
7+
8+
Maybe container
9+
---------------
10+
11+
``Maybe`` consist of two types: ``Some`` and ``Nothing``.
12+
We have a convenient method to create different ``Maybe`` types
13+
based on just a single value:
14+
15+
.. code:: python
16+
17+
from returns.maybe import Maybe
18+
19+
Maybe.new(1)
20+
# => Some(1)
21+
22+
Maybe.new(None)
23+
# => Nothing
24+
25+
26+
Usage
27+
-----
28+
29+
It might be very useful for complex operations like the following one:
30+
31+
.. code:: python
32+
33+
from dataclasses import dataclass
34+
from typing import Optional
35+
36+
@dataclass
37+
class Address(object):
38+
street: Optional[str]
39+
40+
@dataclass
41+
class User(object):
42+
address: Optional[Address]
43+
44+
@dataclass
45+
class Order(object):
46+
user: Optional[User]
47+
48+
order: Order # some existing Order instance
49+
street: Maybe[str] = Maybe.new(order.user).map(
50+
lambda user: user.address,
51+
).map(
52+
lambda address: address.street,
53+
)
54+
# => `Some('address street info')` if all fields are not None
55+
# => `Nothing` if at least one field is `None`
56+
57+
Optional type
58+
~~~~~~~~~~~~~
59+
60+
One may ask: "How is that different to the ``Optional[]`` type?"
61+
That's a really good question!
62+
63+
Consider the same code to get the street name
64+
without ``Maybe`` and using raw ``Optional`` values:
65+
66+
.. code:: python
67+
68+
order: Order # some existing Order instance
69+
street: Optional[str] = None
70+
if order.user is not None:
71+
if order.user.address is not None:
72+
street = order.user.address.street
73+
74+
It looks way uglier and can grow even more uglier and complex
75+
when new logic will be introduced.
76+
77+
78+
@maybe decorator
79+
----------------
80+
81+
Sometimes we have to deal with functions
82+
that dears to return ``Optional`` values!
83+
84+
We have to work with the carefully and write ``if x is not None:`` everywhere.
85+
Luckily, we have your back! ``maybe`` function decorates
86+
any other function that returns ``Optional``
87+
and converts it to return ``Maybe`` instead:
88+
89+
.. code:: python
90+
91+
from typing import Optional
92+
from returns.maybe import Maybe, maybe
93+
94+
@maybe
95+
def number(num: int) -> Optional[int]:
96+
if number > 0:
97+
return num
98+
return None
99+
100+
result: Maybe[int] = number(1)
101+
# => 1
102+
103+
104+
API Reference
105+
-------------
106+
107+
.. autoclasstree:: returns.maybe
108+
109+
.. automodule:: returns.maybe
110+
:members:

returns/__init__.py

Lines changed: 14 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -15,17 +15,12 @@
1515

1616
from returns.functions import compose, raise_exception
1717
from returns.io import IO, impure
18+
from returns.maybe import Maybe, Nothing, Some, maybe
19+
from returns.pipeline import is_successful, pipeline
1820
from returns.primitives.exceptions import UnwrapFailedError
19-
from returns.result import (
20-
Failure,
21-
Result,
22-
Success,
23-
is_successful,
24-
pipeline,
25-
safe,
26-
)
21+
from returns.result import Failure, Result, Success, safe
2722

28-
__all__ = ( # noqa: Z410
23+
__all__ = (
2924
# Functions:
3025
'compose',
3126
'raise_exception',
@@ -34,12 +29,20 @@
3429
'IO',
3530
'impure',
3631

32+
# Maybe:
33+
'Some',
34+
'Nothing',
35+
'Maybe',
36+
'maybe',
37+
3738
# Result:
38-
'is_successful',
3939
'safe',
40-
'pipeline',
4140
'Failure',
4241
'Result',
4342
'Success',
4443
'UnwrapFailedError',
44+
45+
# pipeline:
46+
'is_successful',
47+
'pipeline',
4548
)

returns/contrib/mypy/decorator_plugin.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
_TYPED_DECORATORS = {
2727
'returns.result.safe',
2828
'returns.io.impure',
29+
'returns.maybe.maybe',
2930
}
3031

3132

0 commit comments

Comments
 (0)