Skip to content

Commit

Permalink
Add tests for Option monad
Browse files Browse the repository at this point in the history
  • Loading branch information
ashuping committed Aug 12, 2024
1 parent 95c178b commit 1d16414
Show file tree
Hide file tree
Showing 2 changed files with 136 additions and 5 deletions.
28 changes: 23 additions & 5 deletions modules/types/Option.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ class Option[A](ABC):
def has_val(self) -> bool:
''' Return True if this is `Some[A]`; otherwise, return False.
'''
...
... # pragma: no cover

@property
@abstractmethod
Expand All @@ -43,7 +43,7 @@ def val(self) -> A:
@warning Raises a TypeError if called on `Nothing`.
'''
...
... # pragma: no cover

@abstractmethod
def map(self, f: Callable[[A], B]) -> 'Option[B]':
Expand All @@ -52,15 +52,27 @@ def map(self, f: Callable[[A], B]) -> 'Option[B]':
If it is `Nothing`, do not call `f` and just return `Nothing`.
'''
...
... # pragma: no cover

@abstractmethod
def flat_map(self, f: Callable[[A], 'Option[B]']) -> 'Option[B]':
''' Similar to Map, except that `f` should convert `A`'s directly into
`Option[B]`'s.
'''
...
... # pragma: no cover

def __eq__(self, other):
if self.has_val:
if other.has_val:
return self.val == other.val
else:
return False
else:
if other.has_val:
return False
else:
return True

class Nothing(Option):
@property
Expand All @@ -77,6 +89,9 @@ def map(self, f: Callable[[A], B]) -> Option[B]:
def flat_map(self, f: Callable[[A], Option[B]]) -> Option[B]:
return self

def __str__(self):
return f'Nothing'

class Some[A](Option):
def __init__(self, val: A):
self.__val = val
Expand All @@ -93,4 +108,7 @@ def map(self, f: Callable[[A], B]) -> Option[B]:
return Some(f(self.__val))

def flat_map(self, f: Callable[[A], Option[B]]) -> Option[B]:
return f(self.__val)
return f(self.__val)

def __str__(self):
return f'Some[{self.val.__class__.__name__}]({self.val})'
113 changes: 113 additions & 0 deletions test/types/test_Option.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
from pytest import raises

from modules.types.Option import Option, Nothing, Some

def test_has_val():
assert not Nothing().has_val
assert Some(37).has_val
assert Some("str").has_val
assert Some(None).has_val

def test_val():
assert Some(37).val == 37
assert Some("str").val == "str"
assert Some(None).val == None

with raises(TypeError):
Nothing().val

def test_str():
assert str(Some(37)) == "Some[int](37)"
assert str(Some("test")) == "Some[str](test)"
assert str(Some(None)) == "Some[NoneType](None)"

assert str(Nothing()) == "Nothing"

def test_eq():
assert Some(37) == Some(37)
assert Some("test") == Some("test")
assert Some(None) == Some(None)

assert Some(37) != Some(36)
assert Some("test") != Some("tes")
assert Some(37) != Some("37")

assert Some(37) != Nothing()
assert Some("test") != Nothing()
assert Some(None) != Nothing()

assert Nothing() != Some(37)
assert Nothing() != Some("test")
assert Nothing() != Some(None)

assert Nothing() == Nothing()

def test_map_some():
transform = lambda x: f"my {x}"

assert Some(37).map(transform) == Some("my 37")
assert Some("test").map(transform) == Some("my test")
assert Some(None).map(transform) == Some("my None")

def test_chain_map_some():
tf_a = lambda x: f"precious {x}"
tf_b = lambda x: f"my {x}"

assert Some(37) .map(tf_a).map(tf_b) == Some("my precious 37")
assert Some("test").map(tf_a).map(tf_b) == Some("my precious test")
assert Some(None) .map(tf_a).map(tf_b) == Some("my precious None")

def test_map_nothing():
transform = lambda x: f"my {x}"

assert Nothing().map(transform) == Nothing()

def test_chain_map_nothing():
tf_a = lambda x: f"precious {x}"
tf_b = lambda x: f"my {x}"

assert Nothing().map(tf_a).map(tf_b) == Nothing()

def test_flat_map_some():
transform = lambda x: Some(f"my {x}")

assert Some(37).flat_map(transform) == Some("my 37")
assert Some("test").flat_map(transform) == Some("my test")
assert Some(None).flat_map(transform) == Some("my None")

def test_flat_map_nothing():
transform = lambda x: Some(f"my {x}")

assert Nothing().flat_map(transform) == Nothing()

def test_flat_map_and_map_long_chain():
def tf_stoi(x: str) -> Option[int]:
try:
return Some(int(x))
except Exception as e:
return Nothing()

def tf_double(x: int) -> Option[int]:
return Some(2*x)

def tf_invert(x: int) -> Option[float]:
try:
return Some(1/x)
except Exception as e:
return Nothing()

def tf_stringify(x: float) -> str:
return f"wow, it's {x:.2}"

assert Some("37") \
.flat_map(tf_stoi) \
.flat_map(tf_double) \
.flat_map(tf_invert) \
.map(tf_stringify) == Some("wow, it's 0.014")

assert Some("0") \
.flat_map(tf_stoi) \
.flat_map(tf_double) \
.flat_map(tf_invert) \
.map(tf_stringify) == Nothing(), \
"`map` should be skipped, since `tf_invert` returns Nothing()."

0 comments on commit 1d16414

Please sign in to comment.