diff --git a/src/pyella/either.py b/src/pyella/either.py index 8d53aa3..8e9b4c3 100644 --- a/src/pyella/either.py +++ b/src/pyella/either.py @@ -71,21 +71,39 @@ def fmap(self, apply: Callable[[TB_co], TC_co]) -> Either[TA_co, TC_co]: def if_left( self, - fallback: TB_co, # type: ignore [misc] # covariant arg ok, b/c function is pure + fallback_value_or_fn: TB_co | Callable[[TA_co], TB_co], # type: ignore [misc] # covariant arg ok, b/c function is pure ) -> TB_co: """ Alias for :py:func:`if_left(self, fallback) ` """ - return if_left(self, fallback) + return if_left(self, fallback_value_or_fn) + + def if_left_fn( + self, + fn: Callable[[TA_co], TB_co], # type: ignore [misc] # covariant arg ok, b/c function is pure + ) -> TB_co: + """ + Alias for :py:func:`if_left_fn(self, fn) ` + """ + return if_left_fn(self, fn) def if_right( self, - fallback: TA_co, # type: ignore [misc] # covariant arg ok, b/c function is pure + fallback_value_or_fn: TA_co | Callable[[TB_co], TA_co], # type: ignore [misc] # covariant arg ok, b/c function is pure ) -> TA_co: """ Alias for :py:func:`if_right(self, fallback) ` """ - return if_right(self, fallback) + return if_right(self, fallback_value_or_fn) + + def if_right_fn( + self, + fn: Callable[[TB_co], TA_co], # type: ignore [misc] # covariant arg ok, b/c function is pure + ) -> TA_co: + """ + Alias for :py:func:`if_right_fn(self, fn) ` + """ + return if_right_fn(self, fn) def is_left(self) -> bool: """ @@ -276,26 +294,68 @@ def right_type_helper(value: TB_co) -> Either[TC_co, TB_co]: # type: ignore [mi def if_left( em0: Either[TA_co, TB_co], - fallback: TB_co, # type: ignore [misc] # covariant arg ok, b/c function is pure + fallback_value_or_fn: TB_co | Callable[[TA_co], TB_co], # type: ignore [misc] # covariant arg ok, b/c function is pure ) -> TB_co: """ Return the contents of a :py:class:`Right[TB] ` or a fallback value if it's :py:class:`Left` .. note:: Haskell: `fromRight `_ """ - return fallback if em0.is_left() else cast(TB_co, em0.value) + if not callable(fallback_value_or_fn): + return fallback_value_or_fn if em0.is_left() else cast(TB_co, em0.value) + + return if_left_fn( + em0, + cast( + Callable[[TA_co], TB_co], + fallback_value_or_fn, + ), + ) + + +def if_left_fn( + em0: Either[TA_co, TB_co], + fn: Callable[[TA_co], TB_co], # type: ignore [misc] # covariant arg ok, b/c function is pure +) -> TB_co: + """ + Return the contents of a :py:class:`Right[TB] ` or a mapped left value if it's :py:class:`Left` + + This is a convenience function which doesn't exist in the Haskell implementation + """ + return fn(cast(TA_co, em0.value)) if em0.is_left() else cast(TB_co, em0.value) def if_right( em0: Either[TA_co, TB_co], - fallback: TA_co, # type: ignore [misc] # covariant arg ok, b/c function is pure + fallback_value_or_fn: TA_co | Callable[[TB_co], TA_co], # type: ignore [misc] # covariant arg ok, b/c function is pure ) -> TA_co: """ Return the contents of a :py:class:`Left` or a fallback value if it's :py:class:`Right` .. note:: Haskell: `fromLeft `_ """ - return fallback if em0.is_right() else cast(TA_co, em0.value) + if not callable(fallback_value_or_fn): + return fallback_value_or_fn if em0.is_right() else cast(TA_co, em0.value) + + return if_right_fn( + em0, + cast( + Callable[[TB_co], TA_co], + fallback_value_or_fn, + ), + ) + + +def if_right_fn( + em0: Either[TA_co, TB_co], + fn: Callable[[TB_co], TA_co], # type: ignore [misc] # covariant arg ok, b/c function is pure +) -> TA_co: + """ + Return the contents of a :py:class:`Left` or a mapped right value if it's :py:class:`Right` + + This is a convenience function which doesn't exist in the Haskell implementation + """ + return fn(cast(TB_co, em0.value)) if em0.is_right() else cast(TA_co, em0.value) def is_left(em0: Either[TA_co, TB_co]) -> bool: diff --git a/src/tests/test_either.py b/src/tests/test_either.py index f80a23d..1be88c1 100644 --- a/src/tests/test_either.py +++ b/src/tests/test_either.py @@ -276,6 +276,200 @@ def some_function_not_returning_either(value): "Calling `bind` with function that doesn't return Either should throw an ArgumentTypeError", ) + def test_if_left_returns_expected_results(self): + # arrange + some_value = random_str() + some_fallback_value = random_str() + some_fallback_fn = lambda v: str(v) + "!" + + some_left_value = random_int() + + some_right: Either[int, str] = Either.pure(some_value) + some_left: Either[int, str] = left(some_left_value) + + # act + right_result_from_value = some_right.if_left(some_fallback_value) + right_result_from_fn = some_right.if_left(some_fallback_fn) + left_result_from_value = some_left.if_left(some_fallback_value) + left_result_from_fn = some_left.if_left(some_fallback_fn) + + # assert + self.assertIsInstance( + right_result_from_value, + str, + "Calling `if_left` with fallback value on Right should return unaltered Right value", + ) + self.assertIsInstance( + right_result_from_fn, + str, + "Calling `if_left` with Callable on Left should return unaltered Right value", + ) + self.assertIsInstance( + left_result_from_value, + str, + "Calling `if_left` with fallback value on Left should return fallback value", + ) + self.assertIsInstance( + left_result_from_fn, + str, + "Calling `if_left` with Callable on Left should return fallback value", + ) + + self.assertEqual( + some_value, + right_result_from_value, + "Calling `if_left` on Right should return unaltered Right value", + ) + self.assertEqual( + some_value, + right_result_from_fn, + "Calling `if_left` on Right with Callable should return unaltered Right value", + ) + self.assertEqual( + some_fallback_value, + left_result_from_value, + "Calling `if_left` on Left should return unalterted fallback value", + ) + self.assertEqual( + some_fallback_fn(some_left_value), + left_result_from_fn, + "Calling `if_left` on Left with Callable should return mapped Left value", + ) + + def test_if_right_returns_expected_results(self): + # arrange + some_value = random_int() + some_fallback_value = random_int() + some_fallback_fn = lambda v: v * 2 + + some_left_value = random_int() + + some_right: Either[int, str] = Either.pure(some_value) + some_left: Either[int, str] = left(some_left_value) + + # act + right_result_from_value = some_right.if_right(some_fallback_value) + right_result_from_fn = some_right.if_right(some_fallback_fn) + left_result_from_value = some_left.if_right(some_fallback_value) + left_result_from_fn = some_left.if_right(some_fallback_fn) + + # assert + self.assertIsInstance( + right_result_from_value, + int, + "Calling `if_right` with fallback value on Right should return unaltered Right value", + ) + self.assertIsInstance( + right_result_from_fn, + int, + "Calling `if_right` with Callable on Left should return unaltered Right value", + ) + self.assertIsInstance( + left_result_from_value, + int, + "Calling `if_right` with fallback value on Left should return fallback value", + ) + self.assertIsInstance( + left_result_from_fn, + int, + "Calling `if_right` with Callable on Left should return fallback value", + ) + + self.assertEqual( + some_fallback_value, + right_result_from_value, + "Calling `if_right` on Right should return unalterted fallback value", + ) + self.assertEqual( + some_fallback_fn(some_value), + right_result_from_fn, + "Calling `if_right` on Right with Callable should return mapped Left value", + ) + self.assertEqual( + some_left_value, + left_result_from_value, + "Calling `if_right` on Left should return unaltered Left value", + ) + self.assertEqual( + some_left_value, + left_result_from_fn, + "Calling `if_right` on Left with Callable should return unaltered Left value", + ) + + def test_if_left_fn_returns_expected_results(self): + # arrange + some_value = random_str() + some_fallback_fn = lambda v: str(v) + "!" + + some_left_value = random_int() + + some_right: Either[int, str] = Either.pure(some_value) + some_left: Either[int, str] = left(some_left_value) + + # act + right_result_from_fn = some_right.if_left_fn(some_fallback_fn) + left_result_from_fn = some_left.if_left_fn(some_fallback_fn) + + # assert + self.assertIsInstance( + right_result_from_fn, + str, + "Calling `if_left` with Callable on Left should return unaltered Right value", + ) + self.assertIsInstance( + left_result_from_fn, + str, + "Calling `if_left` with Callable on Left should return fallback value", + ) + + self.assertEqual( + some_value, + right_result_from_fn, + "Calling `if_left` on Right with Callable should return unaltered Right value", + ) + self.assertEqual( + some_fallback_fn(some_left_value), + left_result_from_fn, + "Calling `if_left` on Left with Callable should return mapped Left value", + ) + + def test_if_right_fn_returns_expected_results(self): + # arrange + some_value = random_int() + some_fallback_fn = lambda v: v * 2 + + some_left_value = random_int() + + some_right: Either[int, str] = Either.pure(some_value) + some_left: Either[int, str] = left(some_left_value) + + # act + right_result_from_fn = some_right.if_right_fn(some_fallback_fn) + left_result_from_fn = some_left.if_right_fn(some_fallback_fn) + + # assert + self.assertIsInstance( + right_result_from_fn, + int, + "Calling `if_right` with Callable on Left should return unaltered Right value", + ) + self.assertIsInstance( + left_result_from_fn, + int, + "Calling `if_right` with Callable on Left should return fallback value", + ) + + self.assertEqual( + some_fallback_fn(some_value), + right_result_from_fn, + "Calling `if_right` on Right with Callable should return mapped Left value", + ) + self.assertEqual( + some_left_value, + left_result_from_fn, + "Calling `if_right` on Left with Callable should return unaltered Left value", + ) + def test_rights_and_lefts_return_expected_results(self): # arrange right_values = unique_ints()