Skip to content

Commit 19baa37

Browse files
committed
Adds ebind and efmap
1 parent 7973c06 commit 19baa37

File tree

11 files changed

+217
-12
lines changed

11 files changed

+217
-12
lines changed

CHANGELOG.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,14 +8,15 @@ We follow Semantic Versions since the `0.1.0` release.
88

99
- Adds `Maybe` monad
1010
- Adds immutability and `__slots__` to all monads
11-
- Adds `__hash__` support for immutable inner values
1211
- Adds methods to work with failures
1312
- Adds `safe` decorator to convert exceptions to `Either` monad
1413
- Adds `is_successful()` function to detect if your result is a success
1514

1615
### Bugfixes
1716

1817
- Changes the type of `.bind` method for `Success` monad
18+
- Changes how equality works, so now `Failure(1) != Success(1)`
19+
- Changes how new instances created on unused methods
1920

2021
### Misc
2122

docs/pages/monad.rst

Lines changed: 37 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ is used to literally bind two different monads together.
5353
...
5454
5555
result = Success(1).bind(make_http_call)
56-
# => will be equal to either Success[int] or Failure[str]
56+
# => Will be equal to either Success[int] or Failure[str]
5757
5858
So, the rule is: whenever you have some impure functions,
5959
it should return a monad instead.
@@ -69,12 +69,46 @@ to use monads with pure functions.
6969
return state * 2
7070
7171
result = Success(1).fmap(double)
72-
# => will be equal to Success(2)
72+
# => Will be equal to Success(2)
7373
7474
Reverse operations
7575
~~~~~~~~~~~~~~~~~~
7676

77-
TODO: write about ``or_bind`` and ``or_fmap`` values.
77+
We also support two special methods to work with "failed"
78+
monads like ``Failure`` and ``Nothing``:
79+
80+
- :func:`Monad.efmap <dry_monads.primitives.monad.Monad.efmap>` the opposite
81+
of ``fmap`` method that works only when monad is failed
82+
- :func:`Monad.ebind <dry_monads.primitives.monad.Monad.ebind>` the opposite
83+
of ``bind`` method that works only when monad is failed
84+
85+
.. code:: python
86+
87+
from dry_monads.either import Failure
88+
89+
def double(state: int) -> float:
90+
return state * 2.0
91+
92+
Failure(1).efmap(double)
93+
# => Will be equal to Success(2.0)
94+
95+
So, ``efmap`` can be used to fix some fixable errors
96+
during the pipeline execution.
97+
98+
.. code:: python
99+
100+
from dry_monads.either import Either, Failure, Success
101+
102+
def fix(state: Exception) -> Either[int, Exception]:
103+
if isinstance(state, ZeroDivisionError):
104+
return Success(0)
105+
return Failure(state)
106+
107+
Failure(ZeroDivisionError).ebind(fix)
108+
# => Will be equal to Success(0)
109+
110+
``ebind`` can return any monad you want.
111+
It can also be fixed to get your flow on the right track again.
78112

79113
Unwrapping values
80114
~~~~~~~~~~~~~~~~~

dry_monads/either.py

Lines changed: 33 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -58,11 +58,34 @@ def __init__(self, inner_value: ErrorType) -> None:
5858

5959
def fmap(self, function) -> 'Left[ErrorType]':
6060
"""Returns the 'Left' instance that was used to call the method."""
61-
return Left(self._inner_value)
61+
return self
6262

6363
def bind(self, function) -> 'Left[ErrorType]':
6464
"""Returns the 'Left' instance that was used to call the method."""
65-
return Left(self._inner_value)
65+
return self
66+
67+
def efmap(
68+
self,
69+
function: Callable[[ErrorType], NewValueType],
70+
) -> 'Right[NewValueType]':
71+
"""
72+
Applies function to the inner value.
73+
74+
Applies 'function' to the contents of the 'Right' instance
75+
and returns a new 'Right' object containing the result.
76+
'function' should accept a single "normal" (non-monad) argument
77+
and return a non-monad result.
78+
"""
79+
return Right(function(self._inner_value))
80+
81+
def ebind(self, function: Callable[[ErrorType], MonadType]) -> MonadType:
82+
"""
83+
Applies 'function' to the result of a previous calculation.
84+
85+
'function' should accept a single "normal" (non-monad) argument
86+
and return either a 'Left' or 'Right' type object.
87+
"""
88+
return function(self._inner_value)
6689

6790
def value_or(self, default_value: NewValueType) -> NewValueType:
6891
"""Returns the value if we deal with 'Right' or default if 'Left'."""
@@ -115,6 +138,14 @@ def bind(
115138
"""
116139
return function(self._inner_value)
117140

141+
def efmap(self, function) -> 'Right[ValueType]':
142+
"""Returns the 'Right' instance that was used to call the method."""
143+
return self
144+
145+
def ebind(self, function) -> 'Right[ValueType]':
146+
"""Returns the 'Right' instance that was used to call the method."""
147+
return self
148+
118149
def value_or(self, default_value: NewValueType) -> ValueType:
119150
"""Returns the value if we deal with 'Right' or default if 'Left'."""
120151
return self._inner_value

dry_monads/maybe.py

Lines changed: 36 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -54,11 +54,37 @@ def __init__(self, inner_value: Literal[None] = None) -> None:
5454

5555
def fmap(self, function) -> 'Nothing':
5656
"""Returns the 'Nothing' instance that was used to call the method."""
57-
return Nothing(self._inner_value)
57+
return self
5858

5959
def bind(self, function) -> 'Nothing':
6060
"""Returns the 'Nothing' instance that was used to call the method."""
61-
return Nothing(self._inner_value)
61+
return self
62+
63+
def efmap(
64+
self,
65+
function: Callable[[Literal[None]], 'NewValueType'],
66+
) -> 'Some[NewValueType]':
67+
"""
68+
Applies function to the inner value.
69+
70+
Applies 'function' to the contents of the 'Some' instance
71+
and returns a new 'Some' object containing the result.
72+
'function' should accept a single "normal" (non-monad) argument
73+
and return a non-monad result.
74+
"""
75+
return Some(function(self._inner_value))
76+
77+
def ebind(
78+
self,
79+
function: Callable[[Literal[None]], MonadType],
80+
) -> MonadType:
81+
"""
82+
Applies 'function' to the result of a previous calculation.
83+
84+
'function' should accept a single "normal" (non-monad) argument
85+
and return either a 'Nothing' or 'Some' type object.
86+
"""
87+
return function(self._inner_value)
6288

6389
def value_or(self, default_value: NewValueType) -> NewValueType:
6490
"""Returns the value if we deal with 'Some' or default if 'Nothing'."""
@@ -111,6 +137,14 @@ def bind(
111137
"""
112138
return function(self._inner_value)
113139

140+
def efmap(self, function) -> 'Some[ValueType]':
141+
"""Returns the 'Some' instance that was used to call the method."""
142+
return self
143+
144+
def ebind(self, function) -> 'Some[ValueType]':
145+
"""Returns the 'Some' instance that was used to call the method."""
146+
return self
147+
114148
def value_or(self, default_value: NewValueType) -> ValueType:
115149
"""Returns the value if we deal with 'Some' or default if 'Nothing'."""
116150
return self._inner_value

dry_monads/primitives/monad.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,8 @@ def __eq__(self, other) -> bool:
3434
"""Used to compare two 'Monad' objects."""
3535
if not isinstance(other, _BaseMonad):
3636
return False
37+
if type(self) != type(other):
38+
return False
3739
return self._inner_value == other._inner_value # noqa: Z441
3840

3941

@@ -60,6 +62,8 @@ def fmap(self, function): # pragma: no cover
6062
Applies 'function' to the contents of the functor.
6163
6264
And returns a new functor value.
65+
Works for monads that represent success.
66+
Is the opposite of :meth:`~efmap`.
6367
"""
6468
raise NotImplementedError()
6569

@@ -69,6 +73,30 @@ def bind(self, function): # pragma: no cover
6973
Applies 'function' to the result of a previous calculation.
7074
7175
And returns a new monad.
76+
Works for monads that represent success.
77+
Is the opposite of :meth:`~ebind`.
78+
"""
79+
raise NotImplementedError()
80+
81+
@abstractmethod
82+
def efmap(self, function): # pragma: no cover
83+
"""
84+
Applies 'function' to the contents of the functor.
85+
86+
And returns a new functor value.
87+
Works for monads that represent failure.
88+
Is the opposite of :meth:`~fmap`.
89+
"""
90+
raise NotImplementedError()
91+
92+
@abstractmethod
93+
def ebind(self, function): # pragma: no cover
94+
"""
95+
Applies 'function' to the result of a previous calculation.
96+
97+
And returns a new monad.
98+
Works for monads that represent failure.
99+
Is the opposite of :meth:`~bind`.
72100
"""
73101
raise NotImplementedError()
74102

tests/test_either/test_either_bind.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,3 +25,26 @@ def factory(inner_value: int) -> Left[TypeError]:
2525

2626
assert bound == Left(input_value)
2727
assert str(bound) == 'Left: 5'
28+
29+
30+
def test_ebind_success():
31+
"""Ensures that ebind works for Right monad."""
32+
def factory(inner_value: int) -> Right[int]:
33+
return Right(inner_value * 2)
34+
35+
bound = Right(5).ebind(factory)
36+
37+
assert bound == Right(5)
38+
assert str(bound) == 'Right: 5'
39+
40+
41+
def test_ebind_failure():
42+
"""Ensures that ebind works for Right monad."""
43+
def factory(inner_value: int) -> Left[float]:
44+
return Left(float(inner_value + 1))
45+
46+
expected = 6.0
47+
bound = Left(5).ebind(factory)
48+
49+
assert bound == Left(expected)
50+
assert str(bound) == 'Left: 6.0'

tests/test_either/test_either_equality.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,17 @@ def test_nonequality():
1212

1313
assert Left(input_value) != input_value
1414
assert Right(input_value) != input_value
15+
assert Left(input_value) != Right(input_value)
1516

1617

1718
def test_is_compare():
1819
"""Ensures that `is` operator works correctly."""
19-
assert Right(1) is not Right(1)
20+
left = Left(1)
21+
right = Right(1)
22+
23+
assert left.bind(lambda state: state) is left
24+
assert right.ebind(lambda state: state) is right
25+
assert right is not Right(1)
2026

2127

2228
def test_immutability_failure():

tests/test_either/test_either_fmap.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,3 +11,13 @@ def test_fmap_success():
1111
def test_fmap_failure():
1212
"""Ensures that left identity works for Right monad."""
1313
assert Left(5).fmap(str) == Left(5)
14+
15+
16+
def test_efmap_success():
17+
"""Ensures that left identity works for Right monad."""
18+
assert Right(5).efmap(str) == Right(5)
19+
20+
21+
def test_efmap_failure():
22+
"""Ensures that left identity works for Right monad."""
23+
assert Left(5).efmap(str) == Right('5')

tests/test_maybe/test_maybe_bind.py

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55

66
def test_bind_some():
7-
"""Ensures that Nothing identity works for Some monad."""
7+
"""Ensures that left identity works for Some monad."""
88
def factory(inner_value: int) -> Some[int]:
99
return Some(inner_value * 2)
1010

@@ -16,11 +16,33 @@ def factory(inner_value: int) -> Some[int]:
1616

1717

1818
def test_bind_nothing():
19-
"""Ensures that Nothing identity works for Some monad."""
19+
"""Ensures that left identity works for Some monad."""
2020
def factory(inner_value: None) -> Some[int]:
2121
return Some(1)
2222

2323
bound = Nothing().bind(factory)
2424

2525
assert bound == Nothing(None)
2626
assert str(bound) == 'Nothing: None'
27+
28+
29+
def test_ebind_some():
30+
"""Ensures that left identity works for Some monad."""
31+
def factory(inner_value: int) -> Some[int]:
32+
return Some(inner_value * 2)
33+
34+
bound = Some(5).ebind(factory)
35+
36+
assert bound == Some(5)
37+
assert str(bound) == 'Some: 5'
38+
39+
40+
def test_ebind_nothing():
41+
"""Ensures that left identity works for Some monad."""
42+
def factory(inner_value: None) -> Some[int]:
43+
return Some(1)
44+
45+
bound = Nothing().ebind(factory)
46+
47+
assert bound == Some(1)
48+
assert str(bound) == 'Some: 1'

tests/test_maybe/test_maybe_equality.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,17 @@ def test_nonequality():
1111
assert Nothing() is not None
1212
assert Nothing(None) is not None
1313
assert Some(5) != 5
14+
assert Some(None) != Nothing()
1415

1516

1617
def test_is_compare():
1718
"""Ensures that `is` operator works correctly."""
18-
assert Some(1) is not Some(1)
19+
nothing = Nothing()
20+
some_monad = Some(1)
21+
22+
assert nothing.bind(lambda state: state) is nothing
23+
assert some_monad.ebind(lambda state: state) is some_monad
24+
assert some_monad is not Some(1)
1925

2026

2127
def test_immutability_failure():

tests/test_maybe/test_maybe_fmap.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,3 +11,13 @@ def test_fmap_some():
1111
def test_fmap_nothing():
1212
"""Ensures that fmap works for Nothing monad."""
1313
assert Nothing().fmap(str) == Nothing(None)
14+
15+
16+
def test_efmap_some():
17+
"""Ensures that efmap works for Some monad."""
18+
assert Some(5).efmap(str) == Some(5)
19+
20+
21+
def test_efmap_nothing():
22+
"""Ensures that efmap works for Nothing monad."""
23+
assert Nothing().efmap(lambda state: str(state)) == Some('None')

0 commit comments

Comments
 (0)