Skip to content

Commit

Permalink
feat: add callable if_left and if_right
Browse files Browse the repository at this point in the history
  • Loading branch information
edeckers committed Jan 9, 2025
1 parent 3eed99f commit 4152832
Show file tree
Hide file tree
Showing 2 changed files with 262 additions and 8 deletions.
76 changes: 68 additions & 8 deletions src/pyella/either.py
Original file line number Diff line number Diff line change
Expand Up @@ -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) <if_left>`
"""
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) <if_left>`
"""
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) <if_right>`
"""
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) <if_right>`
"""
return if_right_fn(self, fn)

def is_left(self) -> bool:
"""
Expand Down Expand Up @@ -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] <Right>` or a fallback value if it's :py:class:`Left`
.. note:: Haskell: `fromRight <https://hackage.haskell.org/package/base/docs/Data-Either.html#v: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] <Right>` 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 <https://hackage.haskell.org/package/base/docs/Data-Either.html#v: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:
Expand Down
194 changes: 194 additions & 0 deletions src/tests/test_either.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down

0 comments on commit 4152832

Please sign in to comment.