From 4152832f507772cf34e75eaa762378f0c1e9d025 Mon Sep 17 00:00:00 2001 From: Ely Deckers Date: Thu, 9 Jan 2025 13:39:19 +0100 Subject: [PATCH 1/2] feat: add callable if_left and if_right --- src/pyella/either.py | 76 +++++++++++++-- src/tests/test_either.py | 194 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 262 insertions(+), 8 deletions(-) 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() From 7307e300651e0b7d4ca8ae874c15db74336c8f77 Mon Sep 17 00:00:00 2001 From: Ely Deckers Date: Thu, 9 Jan 2025 14:48:46 +0100 Subject: [PATCH 2/2] fix: remove callable if_left and if_right --- src/pyella/either.py | 34 ++++++----------------- src/tests/test_either.py | 58 ++++++---------------------------------- 2 files changed, 16 insertions(+), 76 deletions(-) diff --git a/src/pyella/either.py b/src/pyella/either.py index 8e9b4c3..0cef6f9 100644 --- a/src/pyella/either.py +++ b/src/pyella/either.py @@ -71,12 +71,12 @@ def fmap(self, apply: Callable[[TB_co], TC_co]) -> Either[TA_co, TC_co]: def if_left( self, - fallback_value_or_fn: TB_co | Callable[[TA_co], TB_co], # type: ignore [misc] # covariant arg ok, b/c function is pure + fallback: 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_value_or_fn) + return if_left(self, fallback) def if_left_fn( self, @@ -89,12 +89,12 @@ def if_left_fn( def if_right( self, - fallback_value_or_fn: TA_co | Callable[[TB_co], TA_co], # type: ignore [misc] # covariant arg ok, b/c function is pure + fallback: 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_value_or_fn) + return if_right(self, fallback) def if_right_fn( self, @@ -294,23 +294,14 @@ 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_value_or_fn: TB_co | Callable[[TA_co], TB_co], # type: ignore [misc] # covariant arg ok, b/c function is pure + fallback: 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 `_ """ - 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, - ), - ) + return fallback if em0.is_left() else cast(TB_co, em0.value) def if_left_fn( @@ -327,23 +318,14 @@ def if_left_fn( def if_right( em0: Either[TA_co, TB_co], - fallback_value_or_fn: TA_co | Callable[[TB_co], TA_co], # type: ignore [misc] # covariant arg ok, b/c function is pure + fallback: 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 `_ """ - 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, - ), - ) + return fallback if em0.is_right() else cast(TA_co, em0.value) def if_right_fn( diff --git a/src/tests/test_either.py b/src/tests/test_either.py index 1be88c1..1dd0ae2 100644 --- a/src/tests/test_either.py +++ b/src/tests/test_either.py @@ -280,7 +280,6 @@ 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() @@ -289,9 +288,7 @@ def test_if_left_returns_expected_results(self): # 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( @@ -299,48 +296,27 @@ def test_if_left_returns_expected_results(self): 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_value = random_str() some_fallback_value = random_int() - some_fallback_fn = lambda v: v * 2 some_left_value = random_int() @@ -349,9 +325,7 @@ def test_if_right_returns_expected_results(self): # 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( @@ -359,47 +333,29 @@ def test_if_right_returns_expected_results(self): 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) + "!" + + def some_fallback_fn(v: int) -> str: + return str(v) + "!" some_left_value = random_int() @@ -435,8 +391,10 @@ def test_if_left_fn_returns_expected_results(self): def test_if_right_fn_returns_expected_results(self): # arrange - some_value = random_int() - some_fallback_fn = lambda v: v * 2 + some_value = random_str() + + def some_fallback_fn(v: str) -> int: + return len(v) some_left_value = random_int()