Created
May 2, 2024 04:16
-
-
Save RyanBalfanz/23d0ad7bf304fc0ca2bd92d1e8f7720d to your computer and use it in GitHub Desktop.
Monads in Python with Type Hint Annotations
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env python3.12 | |
# -*- coding: utf-8 -*- | |
import typing | |
from dataclasses import dataclass, field | |
@dataclass | |
class Functor[T]: | |
value: T | |
def fmap[R](self, f: typing.Callable[[T], R]): | |
return Functor(f(self.value)) | |
@dataclass | |
class Maybe[T](Functor): | |
value: T | |
@typing.override | |
def fmap[R](self, f: typing.Callable[[T], R]): | |
if self.value is None: | |
return Maybe(None) | |
return Maybe(f(self.value)) | |
@dataclass | |
class Result[T](Functor): | |
value: T | |
exceptions: typing.Tuple[typing.Type[Exception]] = field( | |
repr=False, default=(Exception,) | |
) | |
@typing.override | |
def fmap[R](self, f: typing.Callable[[T], R]): | |
if isinstance(self.value, Exception): | |
return self | |
try: | |
return Result(f(self.value)) | |
except self.exceptions as e: | |
return Result(e) | |
def is_err(self): | |
return isinstance(self.value, self.exceptions) | |
def is_ok(self): | |
return not self.is_err() | |
if __name__ == "__main__": | |
# Maybe protects against NPEs (Null Pointer Exceptions). | |
print(Maybe(None).fmap(lambda x: x.foo)) | |
# Maybe does not protect against exceptions. | |
try: | |
# AttributeError: 'function' object has no attribute 'foo' | |
print(Maybe(object()).fmap(lambda x: x.foo)) | |
except AttributeError: | |
pass | |
else: | |
raise AssertionError("Expected AttributeError") | |
# Result protects against exceptions. | |
print(Result(0).fmap(lambda x: 1 / x)) | |
# Here Result protects against ZeroDivisionError specifically. | |
print(Result(0, exceptions=(ZeroDivisionError,)).fmap(lambda x: 1 / x)) | |
# class SomeOtherException(Exception): ... | |
# def raise_some_other_exception(o: typing.Any) -> typing.NoReturn: | |
# raise SomeOtherException(id(o)) | |
# o = object() | |
# es = (ZeroDivisionError,) | |
# try: | |
# print( | |
# Result(0, exceptions=es).fmap( | |
# lambda _: raise_some_other_exception(o) | |
# ) | |
# ) | |
# except es as e: | |
# raise AssertionError("Expected SomeOtherException") | |
# except Exception as e: | |
# assert e.args[0] == id(o), e.args | |
# Result protects against NPEs, but only because of the exception handling. | |
@dataclass | |
class Thing[T]: | |
value: T | |
def __post_init__(self): | |
del self.value | |
try: | |
print((lambda x: x.value)(Thing(1))) | |
assert False, "This should assertion should not be reached." | |
except AttributeError: | |
pass | |
else: | |
raise AssertionError("Expected AttributeError") | |
print(Result(None).fmap(lambda x: x.foo)) | |
print(Result(object()).fmap(lambda x: x.foo)) | |
# Inspect the result object to check if the result is an exception. | |
r = Result(None).fmap(lambda x: x.value) | |
assert not r.is_ok() and r.is_err() | |
assert type(r.value) is AttributeError, type(r.value) | |
# print(Maybe([1,2,3]).fmap(lambda x: [x, x+1])) |
Author
RyanBalfanz
commented
May 19, 2024
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment