Skip to content

Commit ccbf63e

Browse files
committed
Adds Maybe monad
1 parent 738c942 commit ccbf63e

26 files changed

+524
-22
lines changed

CHANGELOG.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,25 @@
22

33
We follow Semantic Versions since the `0.1.0` release.
44

5+
## Version 0.2.0
6+
7+
### Features
8+
9+
- Adds `Maybe` monad
10+
- Adds immutability and `__slots__` to all monads
11+
- Adds `__hash__` support for immutable inner values
12+
- Adds methods to work with failures
13+
- Adds `safe` decorator to convert exceptions to `Either` monad
14+
- Adds `is_successful()` function to detect if your result is a success
15+
16+
### Bugfixes
17+
18+
- Changes the type of `.bind` method for `Success` monad
19+
20+
### Misc
21+
22+
- Improves docs
23+
524

625
## Version 0.1.1
726

README.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# dry-monads
22

3-
[![wemake.services](https://img.shields.io/badge/%20-wemake.services-green.svg?label=%20&logo=data%3Aimage%2Fpng%3Bbase64%2CiVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAMAAAAoLQ9TAAAABGdBTUEAALGPC%2FxhBQAAAAFzUkdCAK7OHOkAAAAbUExURQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP%2F%2F%2F5TvxDIAAAAIdFJOUwAjRA8xXANAL%2Bv0SAAAADNJREFUGNNjYCAIOJjRBdBFWMkVQeGzcHAwksJnAPPZGOGAASzPzAEHEGVsLExQwE7YswCb7AFZSF3bbAAAAABJRU5ErkJggg%3D%3D)](https://wemake.services) [![Build Status](https://travis-ci.org/sobolevn/dry-monads.svg?branch=master)](https://travis-ci.org/sobolevn/dry-monads) [![Coverage Status](https://coveralls.io/repos/github/sobolevn/dry-monads/badge.svg?branch=master)](https://coveralls.io/github/sobolevn/dry-monads?branch=master) [![Documentation Status](https://readthedocs.org/projects/dry-monads/badge/?version=latest)](https://dry-monads.readthedocs.io/en/latest/?badge=latest) [![Python Version](https://img.shields.io/pypi/pyversions/dry-monads.svg)](https://pypi.org/project/dry-monads/) [![Dependencies Status](https://img.shields.io/badge/dependencies-up%20to%20date-brightgreen.svg)](https://github.com/sobolevn/dry-monads/pulls?utf8=%E2%9C%93&q=is%3Apr%20author%3Aapp%2Fdependabot) [![dry-monads](https://img.shields.io/badge/style-wemake-000000.svg)](https://github.com/sobolevn/dry-monads)
3+
[![wemake.services](https://img.shields.io/badge/%20-wemake.services-green.svg?label=%20&logo=data%3Aimage%2Fpng%3Bbase64%2CiVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAMAAAAoLQ9TAAAABGdBTUEAALGPC%2FxhBQAAAAFzUkdCAK7OHOkAAAAbUExURQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP%2F%2F%2F5TvxDIAAAAIdFJOUwAjRA8xXANAL%2Bv0SAAAADNJREFUGNNjYCAIOJjRBdBFWMkVQeGzcHAwksJnAPPZGOGAASzPzAEHEGVsLExQwE7YswCb7AFZSF3bbAAAAABJRU5ErkJggg%3D%3D)](https://wemake.services) [![Build Status](https://travis-ci.org/sobolevn/dry-monads.svg?branch=master)](https://travis-ci.org/sobolevn/dry-monads) [![Coverage Status](https://coveralls.io/repos/github/sobolevn/dry-monads/badge.svg?branch=master)](https://coveralls.io/github/sobolevn/dry-monads?branch=master) [![Documentation Status](https://readthedocs.org/projects/dry-monads/badge/?version=latest)](https://dry-monads.readthedocs.io/en/latest/?badge=latest) [![Python Version](https://img.shields.io/pypi/pyversions/dry-monads.svg)](https://pypi.org/project/dry-monads/)[![dry-monads](https://img.shields.io/badge/style-wemake-000000.svg)](https://github.com/sobolevn/dry-monads)
44

55

66
Monads for `python` made simple and safe.
@@ -27,13 +27,13 @@ pip install dry-monads
2727
We have several the most iconic monads inside:
2828

2929
- [Result, Failure, and Success](https://dry-monads.readthedocs.io/en/latest/pages/either.html) (also known as `Either`, `Left`, and `Right`)
30-
- `Maybe`, `Some`, and `Nothing` (currently WIP)
31-
- `Just` (currently WIP)
30+
- [Maybe, Some, and Nothing](https://dry-monads.readthedocs.io/en/latest/pages/maybe.html)
3231

3332
We also care about code readability and developer experience,
3433
so we have included some useful features to make your life easier:
3534

3635
- [Do notation](https://dry-monads.readthedocs.io/en/latest/pages/do-notation.html)
36+
- [Helper functions](https://dry-monads.readthedocs.io/en/latest/pages/functions.html)
3737

3838

3939
## Example

docs/index.rst

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,10 @@ Contents
88
:caption: Userguide
99

1010
pages/monad.rst
11+
pages/maybe.rst
1112
pages/either.rst
1213
pages/do-notation.rst
14+
pages/functions.rst
1315

1416
.. toctree::
1517
:maxdepth: 1

docs/pages/do-notation.rst

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,38 @@ And at the same time the produced code is simple and readable.
129129

130130
And that's it!
131131

132+
See also:
133+
- https://dry-rb.org/gems/dry-monads/do-notation/
134+
- https://en.wikibooks.org/wiki/Haskell/do_notation
135+
- https://wiki.haskell.org/Do_notation_considered_harmful
136+
137+
Limitations
138+
-----------
139+
140+
There's one limitation in typing
141+
that we are facing right now
142+
due to `mypy issue <https://github.com/python/mypy/issues/3157>`_:
143+
144+
.. code:: python
145+
146+
from dry_monads.do_notation import do_notation
147+
from dry_monads.either import Success
148+
149+
@do_notation
150+
def function(param: int) -> Success[int]:
151+
return Success(param)
152+
153+
reveal_type(function)
154+
# Actual => def (*Any, **Any) -> dry_monads.either.Right*[builtins.int]
155+
# Expected => def (int) -> dry_monads.either.Right*[builtins.int]
156+
157+
This effect can be reduced with the help of `Design by Contract <https://en.wikipedia.org/wiki/Design_by_contract>`_
158+
with these implementations:
159+
160+
- https://github.com/Parquery/icontract
161+
- https://github.com/orsinium/deal
162+
- https://github.com/deadpixi/contracts
163+
132164
API Reference
133165
-------------
134166

docs/pages/either.rst

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,9 @@ Either
33

44
Also known as ``Result``.
55

6-
What is ``Result``? It is obviously a result of series of computations.
7-
It might return an error with some extra details.
6+
``Result`` is obviously a result of some series of computations.
7+
It might succeed with some resulting value.
8+
Or it might return an error with some extra details.
89

910
``Result`` consist of two types: ``Success`` and ``Failure``.
1011
``Success`` represents successful operation result
@@ -26,7 +27,7 @@ and ``Failure`` indicates that something has failed.
2627
user_search_result = find_user(0) # id 0 does not exist!
2728
# => Failure('User was not found')
2829
29-
When it is useful?
30+
When is it useful?
3031
When you do not want to use exceptions to break your execution scope.
3132
Or when you do not want to use ``None`` to represent empty values,
3233
since it will raise ``TypeError`` somewhere

docs/pages/functions.rst

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
Helper functions
2+
================
3+
4+
We feature several helper functions to make your developer experience better.
5+
6+
is_successful
7+
-------------
8+
9+
:func:`is_succesful <dry_monads.functions.is_successful>` is used to
10+
tell whether or not your monad is a success.
11+
We treat only treat monads that does not throw as a successful ones,
12+
basically: :class:`Right <dry_monads.either.Right>`
13+
and :class:`Some <dry_monads.maybe.Some>`.
14+
15+
.. code:: python
16+
17+
from dry_monads.either import Success, Failure
18+
from dry_monads.functions import is_successful
19+
from dry_monads.maybe import Some, Nothing
20+
21+
is_successful(Some(1)) and is_successful(Success(1))
22+
# => True
23+
24+
is_successful(Nothing) or is_successful(Failure('text'))
25+
# => False
26+
27+
safe
28+
----
29+
30+
:func:`safe <dry_monads.functions.safe>` is used to convert
31+
regular functions that can throw exceptions to functions
32+
that return :class:`Either <dry_monads.either.Either>` monad.
33+
34+
.. code:: python
35+
36+
from dry_monads.functions import safe
37+
38+
@safe
39+
def divide(number: int) -> float:
40+
return number / number
41+
42+
divide(1)
43+
# => Success(1.0)
44+
45+
divide(0)
46+
# => Failure(ZeroDivisionError)
47+
48+
API Reference
49+
-------------
50+
51+
.. automodule:: dry_monads.functions
52+
:members:

docs/pages/maybe.rst

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
Maybe
2+
=====
3+
4+
The ``Maybe`` monad is used when a series of computations
5+
could return ``None`` at any point.
6+
7+
Maybe.new
8+
---------
9+
10+
``Maybe`` consist of two types: ``Some`` and ``Nothing``.
11+
We have a convenient method to create different ``Maybe`` monad types
12+
based on just a single value:
13+
14+
.. code:: python
15+
16+
from dry_monads.maybe import Maybe
17+
18+
Maybe.new(1)
19+
# => Some(1)
20+
21+
Maybe.new(None)
22+
# => Nothing()
23+
24+
Usage
25+
-----
26+
27+
It might be very useful for complex operations like the following one:
28+
29+
.. code:: python
30+
31+
from dataclasses import dataclass
32+
from typing import Optional
33+
34+
@dataclass
35+
class Address(object):
36+
street: Optional[str]
37+
38+
@dataclass
39+
class User(object):
40+
address: Optional[Address]
41+
42+
@dataclass
43+
class Order(object):
44+
user: Optional[User]
45+
46+
order: Order # some existing Order instance
47+
Maybe.new(order.user).bind(
48+
lambda user: Maybe.new(user.address),
49+
).bind(
50+
lambda address: Maybe.new(address.street),
51+
)
52+
# =>
53+
# Will return Some('address street info') only when all keys are not None
54+
# Otherwise will return Nothing
55+
56+
API Reference
57+
-------------
58+
59+
.. autoclasstree:: dry_monads.maybe
60+
61+
.. automodule:: dry_monads.maybe
62+
:members:

docs/pages/monad.rst

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,11 @@ to use monads with pure functions.
7171
result = Success(1).fmap(double)
7272
# => will be equal to Success(2)
7373
74+
Reverse operations
75+
~~~~~~~~~~~~~~~~~~
76+
77+
TODO: write about ``or_bind`` and ``or_fmap`` values.
78+
7479
Unwrapping values
7580
~~~~~~~~~~~~~~~~~
7681

dry_monads/do_notation.py

Lines changed: 4 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -6,23 +6,17 @@
66
from dry_monads.primitives.exceptions import UnwrapFailedError
77
from dry_monads.primitives.types import MonadType
88

9+
# Typing decorators is not an easy task, see:
10+
# https://github.com/python/mypy/issues/3157
11+
912

1013
def do_notation(
1114
function: Callable[..., MonadType],
1215
) -> Callable[..., MonadType]:
1316
"""
1417
Decorator to enable 'do-notation' context.
1518
16-
See example usages below.
17-
18-
.. code:: python
19-
20-
21-
See also:
22-
- https://dry-rb.org/gems/dry-monads/do-notation/
23-
- https://en.wikibooks.org/wiki/Haskell/do_notation
24-
- https://wiki.haskell.org/Do_notation_considered_harmful
25-
19+
Should be used for series of computations that rely on ``.unwrap`` method.
2620
"""
2721
@wraps(function)
2822
def decorator(*args, **kwargs) -> MonadType:

dry_monads/either.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77

88
from dry_monads.primitives.exceptions import UnwrapFailedError
99
from dry_monads.primitives.monad import Monad, NewValueType, ValueType
10+
from dry_monads.primitives.types import MonadType
1011

1112
ErrorType = TypeVar('ErrorType')
1213

@@ -104,8 +105,8 @@ def fmap(
104105

105106
def bind(
106107
self,
107-
function: Callable[[ValueType], Either[NewValueType, ErrorType]],
108-
) -> Either[NewValueType, ErrorType]:
108+
function: Callable[[ValueType], MonadType],
109+
) -> MonadType:
109110
"""
110111
Applies 'function' to the result of a previous calculation.
111112

dry_monads/functions.py

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
# -*- coding: utf-8 -*-
2+
3+
from functools import wraps
4+
from typing import Callable, TypeVar
5+
6+
from dry_monads.either import Either, Failure, Success
7+
from dry_monads.primitives.exceptions import UnwrapFailedError
8+
from dry_monads.primitives.types import MonadType
9+
10+
_ReturnType = TypeVar('_ReturnType')
11+
12+
13+
def is_successful(monad: MonadType) -> bool:
14+
"""Determins if a monad was a success or not."""
15+
try:
16+
monad.unwrap()
17+
except UnwrapFailedError:
18+
return False
19+
else:
20+
return True
21+
22+
23+
def safe(
24+
function: Callable[..., _ReturnType],
25+
) -> Callable[..., Either[_ReturnType, Exception]]:
26+
"""
27+
Decorator to covert exception throwing function to 'Either' monad.
28+
29+
Show be used with care, since it only catches 'Exception' subclasses.
30+
It does not catch 'BaseException' subclasses.
31+
"""
32+
@wraps(function)
33+
def decorator(*args, **kwargs) -> Either[_ReturnType, Exception]:
34+
try:
35+
return Success(function(*args, **kwargs))
36+
except Exception as exc:
37+
return Failure(exc)
38+
return decorator

0 commit comments

Comments
 (0)