File: maybe.rst

package info (click to toggle)
python-returns 0.26.0-1
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid
  • size: 2,652 kB
  • sloc: python: 11,000; makefile: 18
file content (294 lines) | stat: -rw-r--r-- 7,936 bytes parent folder | download | duplicates (2)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
.. _maybe:

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

  >>> assert str(Maybe.from_optional(1)) == '<Some: 1>'
  >>> assert str(Maybe.from_optional(None)) == '<Nothing>'

We also have another method called ``.from_value``
that behaves a bit differently:

.. code:: python

  >>> from returns.maybe import Maybe

  >>> assert str(Maybe.from_value(1)) == '<Some: 1>'
  >>> assert str(Maybe.from_value(None)) == '<Some: None>'

Usage
~~~~~

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

.. code:: python

  >>> from attr import dataclass
  >>> from typing import Optional
  >>> from returns.maybe import Maybe, Nothing

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

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

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

  >>> def get_street_address(order: Order) -> Maybe[str]:
  ...     return Maybe.from_optional(order.user).bind_optional(
  ...         lambda user: user.address,
  ...     ).bind_optional(
  ...         lambda address: address.street,
  ...     )

  >>> with_address = Order(User(Address('Some street')))
  >>> empty_user = Order(None)
  >>> empty_address = Order(User(None))
  >>> empty_street = Order(User(Address(None)))

  >>> str(get_street_address(with_address))  # all fields are not None
  '<Some: Some street>'

  >>> assert get_street_address(empty_user) == Nothing
  >>> assert get_street_address(empty_address) == Nothing
  >>> assert get_street_address(empty_street) == Nothing

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.


Pattern Matching
----------------

``Maybe`` values can be matched using the new feature of Python 3.10,
`Structural Pattern Matching <https://www.python.org/dev/peps/pep-0622/>`_,
see the example below:

.. literalinclude:: ../../tests/test_examples/test_maybe/test_maybe_pattern_matching.py


Decorators
----------

Limitations
~~~~~~~~~~~

Typing will only work correctly
if :ref:`our mypy plugin <mypy-plugins>` is used.
This happens due to `mypy issue <https://github.com/python/mypy/issues/3157>`_.

maybe
~~~~~

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

We have to work with it 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, Some, maybe

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

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


FAQ
---

How can I turn Maybe into Optional again?
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

When working with regular Python,
you might need regular ``Optional[a]`` values.

You can easily get one from your ``Maybe`` container at any point in time:

.. code:: python

  >>> from returns.maybe import Maybe
  >>> assert Maybe.from_optional(1).value_or(None) == 1
  >>> assert Maybe.from_optional(None).value_or(None) == None

As you can see, revealed type of ``.value_or(None)`` is ``Optional[a]``.
Use it a fallback.

How to model absence of value vs presence of None value?
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

Let's say you have this ``dict``: ``values = {'a': 1, 'b': None}``
So, you can have two types of ``None`` here:

- ``values.get('b')``
- ``values.get('c')``

But, they are different!
The first has explicit ``None`` value,
the second one has no given key and ``None`` is used as a default.
You might need to know exactly which case you are dealing with.
For example, in validation.

So, the first thing to remember is that:

.. code:: python

  >>> assert Some(None) != Nothing

There's a special way to work with a type like this:

.. code:: python

  >>> values = {'a': 1, 'b': None}

  >>> assert Maybe.from_value(values).map(lambda d: d.get('a')) == Some(1)
  >>> assert Maybe.from_value(values).map(lambda d: d.get('b')) == Some(None)

In contrast, you can ignore both ``None`` values easily:

.. code:: python

  >>> assert Maybe.from_value(values).bind_optional(
  ...     lambda d: d.get('a'),
  ... ) == Some(1)

  >>> assert Maybe.from_value(values).bind_optional(
  ...     lambda d: d.get('b'),
  ... ) == Nothing

So, how to write a complete check for a value: both present and missing?

.. code:: python

  >>> from typing import Optional, Dict, TypeVar
  >>> from returns.maybe import Maybe, Some, Nothing

  >>> _Key = TypeVar('_Key')
  >>> _Value = TypeVar('_Value')

  >>> def check_key(
  ...    heystack: Dict[_Key, _Value],
  ...    needle: _Key,
  ... ) -> Maybe[_Value]:
  ...     if needle not in heystack:
  ...         return Nothing
  ...     return Maybe.from_value(heystack[needle])  # try with `.from_optional`

  >>> real_values = {'a': 1}
  >>> opt_values = {'a': 1, 'b': None}

  >>> assert check_key(real_values, 'a') == Some(1)
  >>> assert check_key(real_values, 'b') == Nothing
  >>> # Type revealed: returns.maybe.Maybe[builtins.int]

  >>> assert check_key(opt_values, 'a') == Some(1)
  >>> assert check_key(opt_values, 'b') == Some(None)
  >>> assert check_key(opt_values, 'c') == Nothing
  >>> # Type revealed: returns.maybe.Maybe[Union[builtins.int, None]]

Choose wisely between ``.from_value`` and ``.map``,
and ``.from_optional`` and ``.bind_optional``.
They are similar, but do different things.

Note that you can also use :meth:`returns.pipeline.is_successful`
to check if the value is present.

See the
`original issue about Some(None) <https://github.com/dry-python/returns/issues/314>`_
for more details and the full history.

Why there's no IOMaybe?
~~~~~~~~~~~~~~~~~~~~~~~

We do have ``IOResult``, but we don't have ``IOMaybe``. Why?
Because when dealing with ``IO`` there are a lot of possible errors.
And ``Maybe`` represents just ``None`` and the value.

It is not useful for ``IO`` related tasks.
So, use ``Result`` instead, which can represent what happened to your ``IO``.

You can convert ``Maybe`` to ``Result``
and back again with special :ref:`converters`.

Why Maybe does not have alt method?
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

Well, because ``Maybe`` only has a single failed value:
``Nothing`` and it cannot be altered.

But, ``Maybe`` has :meth:`returns.maybe.Maybe.or_else_call` method to call
a passed callback function with zero argument on failed container:

.. code:: python

  >>> from returns.maybe import Some, Nothing

  >>> assert Some(1).or_else_call(lambda: 2) == 1
  >>> assert Nothing.or_else_call(lambda: 2) == 2

This method is unique to ``Maybe`` container.


Further reading
---------------

- `Option Monads in Rust <https://hoverbear.org/blog/option-monads-in-rust/>`_
- `Option overview in TypeScript <https://gcanti.github.io/fp-ts/modules/Option.ts.html>`_
- `Maybe not - Rich Hickey <https://www.youtube.com/watch?v=YR5WdGrpoug>`_


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

.. autoclasstree:: returns.maybe
   :strict:

.. automodule:: returns.maybe
   :members: