This commit is contained in:
itamar 2026-04-10 00:21:44 +02:00
commit 8eb63a0625
Signed by: itamar
SSH key fingerprint: SHA256:Dv6UzB9hN8q8FUgMR/7X3DTFpE/vSB2m05+KNnxM4B0
6 changed files with 1020 additions and 0 deletions

2
.gitignore vendored Normal file
View file

@ -0,0 +1,2 @@
__pycache__/
**/__pycache__/

44
lib/__init__.py Normal file
View file

@ -0,0 +1,44 @@
from .typeclasses import (
TypeVar,
Generic,
Semigroup,
Monoid,
Functor,
Applicative,
Monad,
MonadTransformer,
ListMonoid,
StringMonoid,
SumMonoid,
ProductMonoid,
A, B, C, S, W, R, E, F,
)
from .monads import (
Maybe, Just, Nothing,
Either, Right, Left,
List,
IO,
Writer,
State,
Reader,
MaybeT,
)
from .classmethod import Classmethod, Classmthod
__all__ = [
"TypeVar", "Generic",
"Semigroup", "Monoid", "Functor", "Applicative", "Monad", "MonadTransformer",
"ListMonoid", "StringMonoid", "SumMonoid", "ProductMonoid",
"A", "B", "C", "S", "W", "R", "E", "F",
"Maybe", "Just", "Nothing",
"Either", "Right", "Left",
"List",
"IO",
"Writer",
"State",
"Reader",
"MaybeT",
"Classmethod", "Classmthod",
]

30
lib/classmethod.py Normal file
View file

@ -0,0 +1,30 @@
from __future__ import annotations
class Classmethod:
def __init__(self, func):
self.func = func
self._attribute_name: str = getattr(func, "__name__", "<classmethod>")
def __set_name__(self, owner, name: str):
self._attribute_name = name
def __get__(self, instance, owner=None):
if owner is None:
owner = type(instance)
func = self.func
attr = self._attribute_name
def bound(*args, **kwargs):
return func(owner, *args, **kwargs)
bound.__name__ = attr
bound.__qualname__ = f"{owner.__qualname__}.{attr}"
return bound
def __set__(self, instance, value):
raise AttributeError("Classmethod reassigend")
def __repr__(self) -> str:
return f"Classmethod({self.func!r})"
Classmthod = Classmethod

649
lib/monads.py Normal file
View file

@ -0,0 +1,649 @@
from __future__ import annotations
from abc import abstractmethod
from .typeclasses import (
Applicative,
Generic,
Monad,
MonadTransformer,
Monoid,
ListMonoid,
)
from .classmethod import Classmethod
class Maybe(Monad):
@classmethod
def pure(cls, value) -> "Maybe":
return Just(value)
@classmethod
def of(cls, value) -> "Maybe":
return Nothing() if value is None else Just(value)
@classmethod
def from_either(cls, either: "Either") -> "Maybe":
return Just(either._value) if either.is_right() else Nothing()
@abstractmethod
def bind(self, func) -> "Maybe": ...
@abstractmethod
def is_just(self) -> bool: ...
def is_nothing(self) -> bool:
return not self.is_just()
@abstractmethod
def get_or(self, default): ...
def get_or_raise(self, exc=None):
if self.is_just():
return self.get_or(None)
raise (exc if exc is not None else ValueError("Nothing.get_or_raise"))
def filter(self, predicate) -> "Maybe":
return self.bind(lambda x: Just(x) if predicate(x) else Nothing())
def or_else(self, alternative: "Maybe") -> "Maybe":
return self if self.is_just() else alternative
def to_either(self, error) -> "Either":
if self.is_just():
return Right(self.get_or(None))
return Left(error)
def to_list(self) -> "List":
return List([self.get_or(None)]) if self.is_just() else List([])
def __iter__(self):
if self.is_just():
yield self.get_or(None)
def __bool__(self) -> bool:
return self.is_just()
@abstractmethod
def __eq__(self, other: object) -> bool: ...
@abstractmethod
def __hash__(self) -> int: ...
class Just(Maybe):
__slots__ = ("_value",)
def __init__(self, value):
self._value = value
@classmethod
def pure(cls, value) -> "Just":
return cls(value)
def bind(self, func) -> Maybe:
return func(self._value)
def fmap(self, func) -> Maybe:
return Just(func(self._value))
def apply(self, wrapped_func: Applicative) -> Maybe:
if isinstance(wrapped_func, Just):
return Just(wrapped_func._value(self._value))
return Nothing()
def get_or(self, default):
return self._value
def is_just(self) -> bool:
return True
def __repr__(self) -> str:
return f"Just({self._value!r})"
def __eq__(self, other: object) -> bool:
return isinstance(other, Just) and self._value == other._value
def __hash__(self) -> int:
return hash(("Just", self._value))
class Nothing(Maybe):
_instance = None
def __new__(cls):
if cls._instance is None:
cls._instance = super().__new__(cls)
return cls._instance
@classmethod
def pure(cls, value) -> "Just":
return Just(value)
def bind(self, func) -> "Nothing":
return self
def fmap(self, func) -> "Nothing":
return self
def apply(self, wrapped_func: Applicative) -> "Nothing":
return self
def get_or(self, default):
return default
def is_just(self) -> bool:
return False
def __repr__(self) -> str:
return "Nothing"
def __eq__(self, other: object) -> bool:
return isinstance(other, Nothing)
def __hash__(self) -> int:
return hash("Nothing")
class Either(Monad):
@classmethod
def pure(cls, value) -> "Right":
return Right(value)
@classmethod
def try_(cls, func, *args, **kwargs) -> "Either":
try:
return Right(func(*args, **kwargs))
except Exception as exc:
return Left(exc)
@classmethod
def from_maybe(cls, maybe: Maybe, error) -> "Either":
return Right(maybe.get_or(None)) if maybe.is_just() else Left(error)
@abstractmethod
def bind(self, func) -> "Either": ...
@abstractmethod
def is_right(self) -> bool: ...
def is_left(self) -> bool:
return not self.is_right()
@abstractmethod
def get_or(self, default): ...
def get_or_raise(self):
if self.is_right():
return self.get_or(None)
err = self._error if hasattr(self, "_error") else ValueError("Left")
if isinstance(err, BaseException):
raise err
raise ValueError(err)
@abstractmethod
def map_left(self, func) -> "Either": ...
@abstractmethod
def swap(self) -> "Either": ...
def to_maybe(self) -> Maybe:
return Just(self.get_or(None)) if self.is_right() else Nothing()
def __bool__(self) -> bool:
return self.is_right()
@abstractmethod
def __eq__(self, other: object) -> bool: ...
@abstractmethod
def __hash__(self) -> int: ...
class Right(Either):
__slots__ = ("_value",)
def __init__(self, value):
self._value = value
@classmethod
def pure(cls, value) -> "Right":
return cls(value)
def bind(self, func) -> Either:
try:
return func(self._value)
except Exception as exc:
return Left(exc)
def fmap(self, func) -> Either:
try:
return Right(func(self._value))
except Exception as exc:
return Left(exc)
def apply(self, wrapped_func: Applicative) -> Either:
if isinstance(wrapped_func, Right):
return Right(wrapped_func._value(self._value))
return wrapped_func
def get_or(self, default):
return self._value
def map_left(self, func) -> "Right":
return self
def swap(self) -> "Left":
return Left(self._value)
def is_right(self) -> bool:
return True
def __repr__(self) -> str:
return f"Right({self._value!r})"
def __eq__(self, other: object) -> bool:
return isinstance(other, Right) and self._value == other._value
def __hash__(self) -> int:
return hash(("Right", self._value))
class Left(Either):
__slots__ = ("_error",)
def __init__(self, error):
self._error = error
@classmethod
def pure(cls, value) -> "Right":
return Right(value)
def bind(self, func) -> "Left":
return self
def fmap(self, func) -> "Left":
return self
def apply(self, wrapped_func: Applicative) -> "Left":
return self
def get_or(self, default):
return default
def map_left(self, func) -> "Left":
return Left(func(self._error))
def swap(self) -> "Right":
return Right(self._error)
def is_right(self) -> bool:
return False
def __repr__(self) -> str:
return f"Left({self._error!r})"
def __eq__(self, other: object) -> bool:
return isinstance(other, Left) and self._error == other._error
def __hash__(self) -> int:
return hash(("Left", self._error))
class List(Monad):
__slots__ = ("_values",)
def __init__(self, values=None):
self._values = list(values) if values is not None else []
@classmethod
def pure(cls, value) -> "List":
return cls([value])
@classmethod
def empty(cls) -> "List":
return cls([])
@classmethod
def range(cls, *args) -> "List":
return cls(range(*args))
def bind(self, func) -> "List":
result = []
for v in self._values:
out = func(v)
if isinstance(out, List):
result.extend(out._values)
else:
result.append(out)
return List(result)
def fmap(self, func) -> "List":
return List(func(v) for v in self._values)
def apply(self, wrapped_func: "List") -> "List":
return wrapped_func.bind(lambda f: self.fmap(f))
def filter(self, predicate) -> "List":
return List(v for v in self._values if predicate(v))
def concat(self, other: "List") -> "List":
return List(self._values + other._values)
def head(self) -> Maybe:
return Just(self._values[0]) if self._values else Nothing()
def tail(self) -> "List":
return List(self._values[1:])
def last(self) -> Maybe:
return Just(self._values[-1]) if self._values else Nothing()
def take(self, n: int) -> "List":
return List(self._values[:n])
def drop(self, n: int) -> "List":
return List(self._values[n:])
def zip_with(self, other: "List", func) -> "List":
return List(func(a, b) for a, b in zip(self._values, other._values))
def fold(self, func, initial):
acc = initial
for v in self._values:
acc = func(acc, v)
return acc
def traverse_maybe(self, func) -> Maybe:
results = []
for v in self._values:
m = func(v)
if m.is_nothing():
return Nothing()
results.append(m.get_or(None))
return Just(List(results))
def sequence_maybe(self) -> Maybe:
return self.traverse_maybe(lambda x: x)
def traverse_either(self, func) -> Either:
results = []
for v in self._values:
e = func(v)
if e.is_left():
return e
results.append(e.get_or(None))
return Right(List(results))
def sequence_either(self) -> Either:
return self.traverse_either(lambda x: x)
def __len__(self) -> int:
return len(self._values)
def __iter__(self):
return iter(self._values)
def __getitem__(self, index):
return self._values[index]
def __contains__(self, item) -> bool:
return item in self._values
def __repr__(self) -> str:
return f"List({self._values!r})"
def __eq__(self, other: object) -> bool:
return isinstance(other, List) and self._values == other._values
def __hash__(self) -> int:
return hash(("List", tuple(self._values)))
def __add__(self, other: "List") -> "List":
return self.concat(other)
class IO(Monad):
__slots__ = ("_thunk",)
def __init__(self, thunk):
self._thunk = thunk
@classmethod
def pure(cls, value) -> "IO":
return cls(lambda: value)
@classmethod
def from_callable(cls, func, *args, **kwargs) -> "IO":
return cls(lambda: func(*args, **kwargs))
@classmethod
def sequence(cls, ios) -> "IO":
def run_all():
return List([io.run() for io in ios])
return cls(run_all)
def bind(self, func) -> "IO":
return IO(lambda: func(self._thunk()).run())
def fmap(self, func) -> "IO":
return IO(lambda: func(self._thunk()))
def apply(self, wrapped_func: "IO") -> "IO":
return IO(lambda: wrapped_func._thunk()(self._thunk()))
def then(self, other: "IO") -> "IO":
return IO(lambda: (self._thunk(), other.run())[1])
def run(self):
return self._thunk()
def __repr__(self) -> str:
return "IO(<thunk>)"
class Writer(Monad):
__slots__ = ("_value", "_log")
def __init__(self, value, log: Monoid):
self._value = value
self._log = log
@classmethod
def pure(cls, value) -> "Writer":
return cls(value, ListMonoid([]))
@classmethod
def tell(cls, log: Monoid) -> "Writer":
return cls(None, log)
@classmethod
def with_log(cls, value, log: Monoid) -> "Writer":
return cls(value, log)
def bind(self, func) -> "Writer":
new_writer = func(self._value)
return Writer(new_writer._value, self._log.combine(new_writer._log))
def fmap(self, func) -> "Writer":
return Writer(func(self._value), self._log)
def apply(self, wrapped_func: "Writer") -> "Writer":
return Writer(
wrapped_func._value(self._value),
self._log.combine(wrapped_func._log),
)
def run(self):
return (self._value, self._log)
@property
def value(self):
return self._value
@property
def log(self):
return self._log
def listen(self) -> "Writer":
return Writer((self._value, self._log), self._log)
def censor(self, func) -> "Writer":
return Writer(self._value, func(self._log))
def __repr__(self) -> str:
return f"Writer({self._value!r}, {self._log!r})"
def __eq__(self, other: object) -> bool:
return (
isinstance(other, Writer)
and self._value == other._value
and self._log == other._log
)
def __hash__(self) -> int:
return hash(("Writer", self._value))
class State(Monad):
__slots__ = ("_run",)
def __init__(self, run_func):
self._run = run_func
@classmethod
def pure(cls, value) -> "State":
return cls(lambda s: (value, s))
@classmethod
def get(cls) -> "State":
return cls(lambda s: (s, s))
@classmethod
def put(cls, new_state) -> "State":
return cls(lambda _: (None, new_state))
@classmethod
def modify(cls, func) -> "State":
return cls(lambda s: (None, func(s)))
@classmethod
def gets(cls, func) -> "State":
return cls(lambda s: (func(s), s))
def bind(self, func) -> "State":
def _run(state):
value, new_state = self._run(state)
return func(value)._run(new_state)
return State(_run)
def fmap(self, func) -> "State":
def _run(state):
value, new_state = self._run(state)
return (func(value), new_state)
return State(_run)
def apply(self, wrapped_func: "State") -> "State":
def _run(state):
f, state1 = wrapped_func._run(state)
value, state2 = self._run(state1)
return (f(value), state2)
return State(_run)
def run(self, initial_state):
return self._run(initial_state)
def eval(self, initial_state):
return self._run(initial_state)[0]
def exec(self, initial_state):
return self._run(initial_state)[1]
def __repr__(self) -> str:
return "State(<s → (a, s)>)"
class Reader(Monad):
__slots__ = ("_run",)
def __init__(self, run_func):
self._run = run_func
@classmethod
def pure(cls, value) -> "Reader":
return cls(lambda _: value)
@classmethod
def ask(cls) -> "Reader":
return cls(lambda env: env)
@classmethod
def asks(cls, func) -> "Reader":
return cls(lambda env: func(env))
def bind(self, func) -> "Reader":
return Reader(lambda env: func(self._run(env))._run(env))
def fmap(self, func) -> "Reader":
return Reader(lambda env: func(self._run(env)))
def apply(self, wrapped_func: "Reader") -> "Reader":
return Reader(lambda env: wrapped_func._run(env)(self._run(env)))
def local(self, func) -> "Reader":
return Reader(lambda env: self._run(func(env)))
def run(self, env):
return self._run(env)
def __repr__(self) -> str:
return "Reader(<env → a>)"
class MaybeT(MonadTransformer):
__slots__ = ("_inner",)
def __init__(self, inner: Monad):
self._inner = inner
@classmethod
def pure(cls, value) -> "MaybeT":
raise NotImplementedError("Use MaybeT.pure_with(base_cls, value)")
@classmethod
def pure_with(cls, base_cls, value) -> "MaybeT":
return cls(base_cls.pure(Just(value)))
@classmethod
def nothing_with(cls, base_cls) -> "MaybeT":
return cls(base_cls.pure(Nothing()))
@classmethod
def lift(cls, monad: Monad) -> "MaybeT":
return cls(monad.fmap(Just))
def bind(self, func) -> "MaybeT":
def _step(maybe_value):
if maybe_value.is_nothing():
return self._inner.__class__.pure(Nothing())
return func(maybe_value.get_or(None))._inner
return MaybeT(self._inner.bind(_step))
def fmap(self, func) -> "MaybeT":
return MaybeT(self._inner.fmap(lambda m: m.fmap(func)))
def apply(self, wrapped_func: "MaybeT") -> "MaybeT":
return wrapped_func.bind(lambda f: self.fmap(f))
def or_else(self, alternative: "MaybeT") -> "MaybeT":
def _step(maybe_value):
return self._inner.__class__.pure(maybe_value) if maybe_value.is_just() else alternative._inner
return MaybeT(self._inner.bind(_step))
def run(self) -> Monad:
return self._inner
def __repr__(self) -> str:
return f"MaybeT({self._inner!r})"
def __rshift__(self, func) -> "MaybeT":
return self.bind(func)

291
lib/typeclasses.py Normal file
View file

@ -0,0 +1,291 @@
from __future__ import annotations
from abc import ABC, abstractmethod
class TypeVar:
_registry: dict = {}
def __init__(self, name: str, *constraints, bound=None,
covariant: bool = False, contravariant: bool = False):
if covariant and contravariant:
raise ValueError("the type var is both covariant and contravariant. impossible!!!!!!!!!!!!")
self.name = name
self.constraints = constraints
self.bound = bound
self.covariant = covariant
self.contravariant = contravariant
TypeVar._registry[name] = self
def __repr__(self) -> str:
extras = []
if self.bound:
extras.append(f"bound={self.bound!r}")
if self.covariant:
extras.append("covariant=True")
if self.contravariant:
extras.append("contravariant=True")
if self.constraints:
args = ", ".join(repr(c) for c in self.constraints)
return f"TypeVar({self.name!r}, {args})"
suffix = ", ".join(extras)
return f"TypeVar({self.name!r}{', ' + suffix if suffix else ''})"
def __class_getitem__(cls, item):
return cls
class _GenericAlias:
__slots__ = ("__origin__", "__args__", "__parameters__")
def __init__(self, origin, args):
self.__origin__ = origin
self.__args__ = args if isinstance(args, tuple) else (args,)
self.__parameters__ = tuple(a for a in self.__args__ if isinstance(a, TypeVar))
def __repr__(self) -> str:
def _name(a):
return a.__name__ if hasattr(a, "__name__") else repr(a)
return f"{self.__origin__.__name__}[{', '.join(_name(a) for a in self.__args__)}]"
def __call__(self, *args, **kwargs):
return self.__origin__(*args, **kwargs)
def __instancecheck__(self, instance):
return isinstance(instance, self.__origin__)
def __subclasscheck__(self, subclass):
return issubclass(subclass, self.__origin__)
def __class_getitem__(cls, params):
return cls
def __getitem__(self, params):
return _GenericAlias(self.__origin__, params)
class Generic:
def __class_getitem__(cls, params):
return _GenericAlias(cls, params)
def __init_subclass__(cls, **kwargs):
super().__init_subclass__(**kwargs)
A = TypeVar("A")
B = TypeVar("B")
C = TypeVar("C")
S = TypeVar("S")
W = TypeVar("W")
R = TypeVar("R")
E = TypeVar("E")
F = TypeVar("F")
class Semigroup(ABC):
@abstractmethod
def combine(self, other: "Semigroup") -> "Semigroup":
...
def __add__(self, other: "Semigroup") -> "Semigroup":
return self.combine(other)
class Monoid(Semigroup, ABC):
@classmethod
@abstractmethod
def empty(cls) -> "Monoid":
...
@classmethod
def concat(cls, xs) -> "Monoid":
result = cls.empty()
for x in xs:
result = result.combine(x)
return result
class ListMonoid(Monoid):
def __init__(self, values=None):
self.values = list(values) if values is not None else []
def combine(self, other: "ListMonoid") -> "ListMonoid":
if not isinstance(other, ListMonoid):
raise TypeError(f"Cannot combine ListMonoid with {type(other).__name__}")
return ListMonoid(self.values + other.values)
@classmethod
def empty(cls) -> "ListMonoid":
return cls([])
def __repr__(self) -> str:
return f"ListMonoid({self.values!r})"
def __eq__(self, other: object) -> bool:
return isinstance(other, ListMonoid) and self.values == other.values
def __hash__(self) -> int:
return hash(("ListMonoid", tuple(self.values)))
def __iter__(self):
return iter(self.values)
def __len__(self) -> int:
return len(self.values)
class StringMonoid(Monoid):
def __init__(self, value: str = ""):
self.value = value
def combine(self, other: "StringMonoid") -> "StringMonoid":
if not isinstance(other, StringMonoid):
raise TypeError(f"Cannot combine StringMonoid with {type(other).__name__}")
return StringMonoid(self.value + other.value)
@classmethod
def empty(cls) -> "StringMonoid":
return cls("")
def __repr__(self) -> str:
return f"StringMonoid({self.value!r})"
def __eq__(self, other: object) -> bool:
return isinstance(other, StringMonoid) and self.value == other.value
def __hash__(self) -> int:
return hash(("StringMonoid", self.value))
class SumMonoid(Monoid):
def __init__(self, value=0):
self.value = value
def combine(self, other: "SumMonoid") -> "SumMonoid":
return SumMonoid(self.value + other.value)
@classmethod
def empty(cls) -> "SumMonoid":
return cls(0)
def __repr__(self) -> str:
return f"Sum({self.value})"
def __eq__(self, other: object) -> bool:
return isinstance(other, SumMonoid) and self.value == other.value
def __hash__(self) -> int:
return hash(("Sum", self.value))
class ProductMonoid(Monoid):
def __init__(self, value=1):
self.value = value
def combine(self, other: "ProductMonoid") -> "ProductMonoid":
return ProductMonoid(self.value * other.value)
@classmethod
def empty(cls) -> "ProductMonoid":
return cls(1)
def __repr__(self) -> str:
return f"Product({self.value})"
def __eq__(self, other: object) -> bool:
return isinstance(other, ProductMonoid) and self.value == other.value
def __hash__(self) -> int:
return hash(("Product", self.value))
class Functor(ABC, Generic):
@abstractmethod
def fmap(self, func) -> "Functor":
...
def __mod__(self, func) -> "Functor":
return self.fmap(func)
class Applicative(Functor, ABC):
@classmethod
@abstractmethod
def pure(cls, value) -> "Applicative":
...
@abstractmethod
def apply(self, wrapped_func: "Applicative") -> "Applicative":
...
def map2(self, other: "Applicative", func) -> "Applicative":
return other.apply(self.fmap(lambda a: lambda b: func(a, b)))
def __mul__(self, wrapped_func: "Applicative") -> "Applicative":
return self.apply(wrapped_func)
class Monad(Applicative, ABC):
@abstractmethod
def bind(self, func) -> "Monad":
...
def fmap(self, func) -> "Monad":
return self.bind(lambda x: self.__class__.pure(func(x)))
def apply(self, wrapped_func: "Applicative") -> "Monad":
return wrapped_func.bind(
lambda f: self.bind(lambda x: self.__class__.pure(f(x)))
)
def then(self, other: "Monad") -> "Monad":
return self.bind(lambda _: other)
def __rshift__(self, func) -> "Monad":
return self.bind(func)
def __or__(self, other: "Monad") -> "Monad":
return self.then(other)
@classmethod
def do(cls, gen_func) -> "Monad":
return _drive_do(gen_func)
def _drive_do(gen_func) -> "Monad":
gen = gen_func()
def step(value):
try:
next_monad = gen.send(value)
return next_monad.bind(step)
except StopIteration as exc:
return exc.value
try:
first = next(gen)
return first.bind(step)
except StopIteration as exc:
return exc.value
class MonadTransformer(ABC):
@classmethod
@abstractmethod
def lift(cls, monad: "Monad") -> "MonadTransformer":
...
@abstractmethod
def run(self) -> "Monad":
...
@abstractmethod
def bind(self, func) -> "MonadTransformer":
...
@classmethod
@abstractmethod
def pure(cls, value) -> "MonadTransformer":
...
def __rshift__(self, func) -> "MonadTransformer":
return self.bind(func)

4
test.py Normal file
View file

@ -0,0 +1,4 @@
from lib import List
lst = List.pure(2) >> (lambda n: List([n, n+1]))
print(lst)