Skip to content

WIP: adds Maybe monad #89

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Jun 16, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ We follow Semantic Versions since the `0.1.0` release.

### Features

- Reintroduces the `Maybe` monad, typed!
- Adds `mypy` plugin to type decorators
- Complete rewrite of `Result` types
- Partial API change, now `Success` and `Failure` are not types, but functions
Expand Down
16 changes: 16 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,22 @@ flake8 returns tests docs

These steps are mandatory during the CI.

### Fixing pytest coverage issue

Coverage does not work well with `pytest-mypy-plugin`,
that's why we have two phases of `pytest` run.

If you accidentally mess things up
and see `INTERNALERROR> coverage.misc.CoverageException` in your log,
do:

```bash
rm .coverage*
rm -rf .pytest_cache htmlcov
```

And it should solve it.
Then use correct test commands.

## Type checks

Expand Down
33 changes: 33 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ Make sure you know how to get started, [check out our docs](https://returns.read

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


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


## Maybe container

Have you ever since code with a lot of `if some is not None` conditions?
It really bloats your source code and makes it unreadable.

But, having `None` in your source code is even worth.
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/).

So, what to do? Use `Maybe` container!
It consists of `Some(...)` and `Nothing` types,
representing existing state and `None` state respectively.

```python
from typing import Optional
from returns.maybe import Maybe

def bad_function() -> Optional[int]:
...

maybe_result: Maybe[float] = Maybe.new(
bad_function(),
).map(
lambda number: number / 2
)
# => Maybe will return Some(float) only if there's a non-None value
# Otherwise, will return Nothing
```

Forget about `None`-related errors forever!


## More!

Want more? [Go to the docs!](https://returns.readthedocs.io)
Expand Down
1 change: 1 addition & 0 deletions docs/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ Contents

pages/container.rst
pages/result.rst
pages/maybe.rst
pages/io.rst
pages/unsafe.rst
pages/functions.rst
Expand Down
110 changes: 110 additions & 0 deletions docs/pages/maybe.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
Maybe
=====

The ``Maybe`` container is used when a series of computations
could return ``None`` at any point.


Maybe container
---------------

``Maybe`` consist of two types: ``Some`` and ``Nothing``.
We have a convenient method to create different ``Maybe`` types
based on just a single value:

.. code:: python

from returns.maybe import Maybe

Maybe.new(1)
# => Some(1)

Maybe.new(None)
# => Nothing


Usage
-----

It might be very useful for complex operations like the following one:

.. code:: python

from dataclasses import dataclass
from typing import Optional

@dataclass
class Address(object):
street: Optional[str]

@dataclass
class User(object):
address: Optional[Address]

@dataclass
class Order(object):
user: Optional[User]

order: Order # some existing Order instance
street: Maybe[str] = Maybe.new(order.user).map(
lambda user: user.address,
).map(
lambda address: address.street,
)
# => `Some('address street info')` if all fields are not None
# => `Nothing` if at least one field is `None`

Optional type
~~~~~~~~~~~~~

One may ask: "How is that different to the ``Optional[]`` type?"
That's a really good question!

Consider the same code to get the street name
without ``Maybe`` and using raw ``Optional`` values:

.. code:: python

order: Order # some existing Order instance
street: Optional[str] = None
if order.user is not None:
if order.user.address is not None:
street = order.user.address.street

It looks way uglier and can grow even more uglier and complex
when new logic will be introduced.


@maybe decorator
----------------

Sometimes we have to deal with functions
that dears to return ``Optional`` values!

We have to work with the carefully and write ``if x is not None:`` everywhere.
Luckily, we have your back! ``maybe`` function decorates
any other function that returns ``Optional``
and converts it to return ``Maybe`` instead:

.. code:: python

from typing import Optional
from returns.maybe import Maybe, maybe

@maybe
def number(num: int) -> Optional[int]:
if number > 0:
return num
return None

result: Maybe[int] = number(1)
# => 1


API Reference
-------------

.. autoclasstree:: returns.maybe

.. automodule:: returns.maybe
:members:
25 changes: 14 additions & 11 deletions returns/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,17 +15,12 @@

from returns.functions import compose, raise_exception
from returns.io import IO, impure
from returns.maybe import Maybe, Nothing, Some, maybe
from returns.pipeline import is_successful, pipeline
from returns.primitives.exceptions import UnwrapFailedError
from returns.result import (
Failure,
Result,
Success,
is_successful,
pipeline,
safe,
)
from returns.result import Failure, Result, Success, safe

__all__ = ( # noqa: Z410
__all__ = (
# Functions:
'compose',
'raise_exception',
Expand All @@ -34,12 +29,20 @@
'IO',
'impure',

# Maybe:
'Some',
'Nothing',
'Maybe',
'maybe',

# Result:
'is_successful',
'safe',
'pipeline',
'Failure',
'Result',
'Success',
'UnwrapFailedError',

# pipeline:
'is_successful',
'pipeline',
)
1 change: 1 addition & 0 deletions returns/contrib/mypy/decorator_plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
_TYPED_DECORATORS = {
'returns.result.safe',
'returns.io.impure',
'returns.maybe.maybe',
}


Expand Down
Loading