Skip to content

Commit 8f62bf3

Browse files
committed
Closes #196
1 parent b78b84e commit 8f62bf3

File tree

6 files changed

+130
-0
lines changed

6 files changed

+130
-0
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ See (0Ver)[https://0ver.org/].
1313
- **Breaking**: now `@pipeline` requires a container type when created:
1414
`@pipeline(Result)` or `@pipeline(Maybe)`
1515
- `Maybe` and `Result` now has `success_type` and `failure_type` aliases
16+
- Adds `Result.unify` utility method for better error type composition
1617
- We now support `dry-python/classes` as a first-class citizen
1718
- Adds `io_squash` to squash several `IO` containers into one container
1819
with a tuple inside, currently works with `9` containers max at a time

docs/pages/result.rst

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,30 @@ That's why we have public
136136
type constructor functions: ``Success`` and ``Failure``
137137
and internal implementation.
138138

139+
How to compose error types?
140+
~~~~~~~~~~~~~~~~~~~~~~~~~~~
141+
142+
You might want to sometimes use ``.unify`` instead of ``.bind``
143+
to compose error types together.
144+
While ``.bind`` enforces error type to stay the same,
145+
``.unify`` is designed
146+
to return a ``Union`` of a revious error type and a new one.
147+
148+
Like so:
149+
150+
.. code:: python
151+
152+
from returns.result import Result
153+
154+
def div(number: int) -> Result[float, ZeroDivisionError]:
155+
return 1 / number
156+
157+
container: Result[int, ValueError]
158+
container.unify(div)
159+
# => Revealed type is: Result[float, Union[ValueError, ZeroDivisionError]]
160+
161+
So, that's a way to go, if you need this composition.
162+
139163

140164
API Reference
141165
-------------

returns/result.py

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,22 @@ def bind(
6262
"""Abstract method to compose a container with another container."""
6363
raise NotImplementedError
6464

65+
def unify(
66+
self,
67+
function: Callable[
68+
[_ValueType], 'Result[_NewValueType, _NewErrorType]',
69+
],
70+
) -> 'Result[_NewValueType, Union[_ErrorType, _NewErrorType]]':
71+
"""
72+
Abstract method to compose a container with another container.
73+
74+
Similar to ``.bind``, but unifies the error type into a new type.
75+
It is useful when you have several functions to compose
76+
and each of them raises their own exceptions.
77+
That's a way to collect all of them.
78+
"""
79+
raise NotImplementedError
80+
6581
def fix(
6682
self,
6783
function: Callable[[_ErrorType], _NewValueType],
@@ -160,6 +176,21 @@ def bind(self, function):
160176
"""
161177
return self
162178

179+
def unify(self, function):
180+
"""
181+
Returns the '_Failure' instance that was used to call the method.
182+
183+
.. code:: python
184+
185+
>>> def bindable(string: str) -> Result[str, str]:
186+
... return Success(string + 'b')
187+
...
188+
>>> Failure('a').unify(bindable) == Failure('a')
189+
True
190+
191+
"""
192+
return self
193+
163194
def fix(self, function):
164195
"""
165196
Applies function to the inner value.
@@ -312,6 +343,29 @@ def bind(self, function):
312343
"""
313344
return function(self._inner_value)
314345

346+
def unify(self, function):
347+
"""
348+
Applies 'function' to the result of a previous calculation.
349+
350+
It is the same as ``.bind``, but handles type signatures differently.
351+
While ``.bind`` forces to respect error type and not changing it,
352+
``.unify`` allows to return a ``Union`` of previous
353+
error types and new ones.
354+
355+
'function' should accept a single "normal" (non-container) argument
356+
and return Result a '_Failure' or '_Success' type object.
357+
358+
.. code:: python
359+
360+
>>> def bindable(string: str) -> Result[str, str]:
361+
... return Success(string + 'b')
362+
...
363+
>>> Success('a').bind(bindable) == Success('ab')
364+
True
365+
366+
"""
367+
return self.bind(function) # type: ignore
368+
315369
def fix(self, function):
316370
"""
317371
Returns the '_Success' instance that was used to call the method.

tests/test_result/test_result_base.py

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

88
@pytest.mark.parametrize('method_name', [
99
'bind',
10+
'unify',
1011
'map',
1112
'rescue',
1213
'fix',

tests/test_result/test_result_bind.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,17 @@ def factory(inner_value: int) -> Result[int, str]:
3434
assert bound.bind(factory) == factory(input_value)
3535

3636

37+
def test_unify_and_bind():
38+
"""Ensures that left identity works for Success container."""
39+
def factory(inner_value: int) -> Result[int, str]:
40+
return Success(inner_value * 2)
41+
42+
bound: Result[int, str] = Success(5)
43+
44+
assert bound.unify(factory) == bound.bind(factory)
45+
assert bound.bind(factory) == bound.unify(factory)
46+
47+
3748
def test_left_identity_failure():
3849
"""Ensures that left identity works for Failure container."""
3950
def factory(inner_value: int) -> Result[int, int]:
@@ -43,6 +54,7 @@ def factory(inner_value: int) -> Result[int, int]:
4354
bound: Result[int, int] = Failure(input_value)
4455

4556
assert bound.bind(factory) == Failure(input_value)
57+
assert bound.unify(factory) == Failure(input_value)
4658
assert str(bound) == '<Failure: 5>'
4759

4860

typesafety/test_result/test_success.yml

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,44 @@
1010
reveal_type(first.bind(returns_result)) # N: Revealed type is 'returns.result.Result[builtins.str*, builtins.Exception]'
1111
1212
13+
- case: success_unify1
14+
disable_cache: true
15+
main: |
16+
from returns.result import Success, Result
17+
18+
def returns_result(param: int) -> Result[str, TypeError]:
19+
...
20+
21+
first: Result[int, ValueError] = Success(1)
22+
reveal_type(first.unify(returns_result)) # N: Revealed type is 'returns.result.Result[builtins.str*, Union[builtins.ValueError, builtins.TypeError*]]'
23+
24+
25+
- case: success_unify2
26+
disable_cache: true
27+
main: |
28+
from typing import Union
29+
from returns.result import Success, Result
30+
31+
def returns_result(param: int) -> Result[str, TypeError]:
32+
...
33+
34+
first: Result[int, Union[ValueError, KeyError]] = Success(1)
35+
reveal_type(first.unify(returns_result)) # N: Revealed type is 'returns.result.Result[builtins.str*, Union[builtins.ValueError, builtins.KeyError, builtins.TypeError*]]'
36+
37+
38+
- case: success_unify3
39+
disable_cache: true
40+
main: |
41+
from typing import Union
42+
from returns.result import Success, Result
43+
44+
def returns_result(param: int) -> Result[str, Union[KeyError, TypeError]]:
45+
...
46+
47+
first: Result[int, ValueError] = Success(1)
48+
reveal_type(first.unify(returns_result)) # N: Revealed type is 'returns.result.Result[builtins.str*, Union[builtins.ValueError, builtins.KeyError, builtins.TypeError]]'
49+
50+
1351
- case: success_map
1452
disable_cache: true
1553
main: |

0 commit comments

Comments
 (0)