Skip to content

Commit

Permalink
Add lift2 method
Browse files Browse the repository at this point in the history
  • Loading branch information
HKGx committed Nov 23, 2022
1 parent d7c8b68 commit 23dbc17
Show file tree
Hide file tree
Showing 2 changed files with 65 additions and 14 deletions.
64 changes: 50 additions & 14 deletions perhaps/maybe.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@

T = TypeVar("T")
R = TypeVar("R")
U = TypeVar("U")


class Maybe(Generic[T], ABC):
Expand All @@ -34,6 +35,27 @@ def map(self, f: Callable[[T], R]) -> "Maybe[R]":
"""
...

@abstractmethod
def lift2(self, f: Callable[[T, R], U], other: "Maybe[R]") -> "Maybe[U]":
"""
Given a function that takes two arguments, and another Maybe, apply the
function to the value of this Maybe and the other Maybe, if they both
have values.
Analogous to the `Option::zip_wth` in Rust and the `liftA2` in Haskell.
>>> Just(1).lift2(lambda x, y: x + y, Just(2))
Just(3)
>>> Just(1).lift2(lambda x, y: x + y, Nothing())
Nothing()
>>> Nothing().lift2(lambda x, y: x + y, Just(2))
Nothing()
"""
...

@abstractmethod
def bind(self, f: Callable[[T], "Maybe[R]"]) -> "Maybe[R]":
"""
Expand Down Expand Up @@ -168,10 +190,6 @@ def from_try(
except exc:
return Nothing()

@abstractmethod
def __and__(self, other: "Maybe[R]") -> Union["Maybe[T]", "Maybe[R]"]:
...

@abstractmethod
def __or__(self, other: "Maybe[R]") -> Union["Maybe[T]", "Maybe[R]"]:
...
Expand Down Expand Up @@ -200,10 +218,25 @@ def __init__(self, value: T):
def map(self, f: Callable[[T], R]) -> "Just[R]":
return Just(f(self.value))

def bind(self, f: Callable[[T], "Maybe[R]"]) -> "Maybe[R]":
@overload
def lift2(self, f: Callable[[T, R], U], other: "Just[R]") -> "Just[U]":
...

@overload
def lift2(self, f: Callable[[T, R], U], other: "Nothing[R]") -> "Nothing[U]":
...

@overload
def lift2(self, f: Callable[[T, R], U], other: "Maybe[R]") -> "Maybe[U]":
...

def lift2(self, f: Callable[[T, R], U], other: Maybe[R]) -> Maybe[U]:
return other.map(lambda x: f(self.value, x))

def bind(self, f: Callable[[T], Maybe[R]]) -> Maybe[R]:
return f(self.value)

def and_then(self, f: Callable[[T], "Maybe[R]"]) -> "Maybe[R]":
def and_then(self, f: Callable[[T], Maybe[R]]) -> Maybe[R]:
return self.bind(f)

def unwrap(
Expand Down Expand Up @@ -232,10 +265,10 @@ def __and__(self, other: "Nothing[R]") -> "Nothing[R]":
...

@overload
def __and__(self, other: "Maybe[R]") -> Union["Maybe[T]", "Maybe[R]"]:
def __and__(self, other: Maybe[R]) -> Union[Maybe[T], Maybe[R]]:
...

def __and__(self, other: "Maybe[R]") -> Union["Maybe[T]", "Maybe[R]"]:
def __and__(self, other: Maybe[R]) -> Union[Maybe[T], Maybe[R]]:
return other

@overload
Expand All @@ -247,10 +280,10 @@ def __or__(self, other: "Nothing") -> "Just[T]":
...

@overload
def __or__(self, other: "Maybe[R]") -> Union["Maybe[T]", "Maybe[R]"]:
def __or__(self, other: Maybe[R]) -> Union[Maybe[T], Maybe[R]]:
...

def __or__(self, other: "Maybe[R]") -> Union["Maybe[T]", "Maybe[R]"]:
def __or__(self, other: Maybe[R]) -> Union[Maybe[T], Maybe[R]]:
return self

def __eq__(self, other: object) -> bool:
Expand All @@ -276,10 +309,13 @@ def __new__(cls) -> "Nothing[T]":
def map(self, f: Callable[[T], R]) -> "Nothing[R]":
return Nothing()

def bind(self, f: Callable[[T], "Maybe[R]"]) -> "Nothing[R]":
def lift2(self, f: Callable[[T, R], U], other: Maybe[R]) -> Maybe[U]:
return Nothing()

def bind(self, f: Callable[[T], Maybe[R]]) -> "Nothing[R]":
return Nothing()

def and_then(self, f: Callable[[T], "Maybe[R]"]) -> "Nothing[R]":
def and_then(self, f: Callable[[T], Maybe[R]]) -> "Nothing[R]":
return self.bind(f)

def unwrap(
Expand Down Expand Up @@ -309,7 +345,7 @@ def __and__(self, other: "Just") -> "Nothing[T]":
def __and__(self, other: "Nothing[R]") -> "Nothing[R]":
...

def __and__(self, other: "Maybe[R]") -> Union["Nothing[T]", "Maybe[R]"]:
def __and__(self, other: Maybe[R]) -> Union["Nothing[T]", Maybe[R]]:
return other if isinstance(other, Nothing) else self

@overload
Expand All @@ -320,7 +356,7 @@ def __or__(self, other: "Just[R]") -> "Just[R]":
def __or__(self, other: "Nothing[R]") -> "Nothing[R]":
...

def __or__(self, other: "Maybe[R]") -> Union["Nothing[T]", "Maybe[R]"]:
def __or__(self, other: Maybe[R]) -> Union["Nothing[T]", Maybe[R]]:
return other

def __eq__(self, other: object) -> bool:
Expand Down
15 changes: 15 additions & 0 deletions tests/test_maybe.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
from typing import cast

import pytest

from perhaps import Just, Maybe, Nothing
Expand All @@ -24,6 +26,19 @@ def test_map():
assert Nothing().map(lambda x: x + 1) == Nothing()


def test_lift2():
assert Just(1).lift2(lambda x, y: x + y, Just(2)) == Just(3)
assert (
Just(1).lift2(lambda x, y: x + y, Nothing[int]()) == Nothing()
) # annotation for Nothing is required here, though most often it can be inferred
assert Nothing().lift2(lambda x, y: x + y, Just(2)) == Nothing()
maybe1 = cast(Maybe[int], Nothing())
assert maybe1.lift2(lambda x, y: x + y, Just(2)) == Nothing()
maybe2 = cast(Maybe[int], Just(3))
assert maybe1.lift2(lambda x, y: x + y, maybe2) == Nothing()
assert maybe2.lift2(lambda x, y: x * y, maybe2) == Just(9)


def test_bind():
assert Just(1).bind(lambda x: Just(x + 1)) == Just(2)
assert Nothing().bind(lambda x: Just(x + 1)) == Nothing()
Expand Down

0 comments on commit 23dbc17

Please sign in to comment.