Documentation!

This commit is contained in:
Emi Simpson 2023-02-09 21:28:44 -05:00
parent a018831a49
commit b13531aecf
Signed by: Emi
GPG Key ID: A12F2C2FFDC3D847
1 changed files with 163 additions and 6 deletions

View File

@ -14,6 +14,14 @@ P2 = ParamSpec('P2')
# Compose # Compose
def c(f2: Callable[[B], C], f1: Callable[P, B]) -> Callable[P, C]: def c(f2: Callable[[B], C], f1: Callable[P, B]) -> Callable[P, C]:
"""
Compose two functions by passing the output of the second to the input of the first.
`c(f1, f2)(*args)` is equivalent to `f1(f2(*args))`.
This can also be thought of as mapping the output of a function using the first
parameter as a mapper function.
"""
@wraps(f1) @wraps(f1)
def inner(*args: P.args, **kwargs: P.kwargs) -> C: def inner(*args: P.args, **kwargs: P.kwargs) -> C:
return f2(f1(*args, **kwargs)) return f2(f1(*args, **kwargs))
@ -21,6 +29,12 @@ def c(f2: Callable[[B], C], f1: Callable[P, B]) -> Callable[P, C]:
# Flip: (A -> B -> C) -> B -> A -> C # Flip: (A -> B -> C) -> B -> A -> C
def flip(f: Callable[P1, Callable[P2, C]]) -> Callable[P2, Callable[P1, C]]: def flip(f: Callable[P1, Callable[P2, C]]) -> Callable[P2, Callable[P1, C]]:
"""
Reverse the order of the first two arguments of a curried function.
This only works with curried functions, so apply `cur2` or `cur3` before applying
`flip` if the arguments you want to flip are not curried.
"""
@wraps(f) @wraps(f)
def inner1(*args2: P2.args, **kwargs2: P2.kwargs) -> Callable[P1, C]: def inner1(*args2: P2.args, **kwargs2: P2.kwargs) -> Callable[P1, C]:
@wraps(f) @wraps(f)
@ -31,39 +45,82 @@ def flip(f: Callable[P1, Callable[P2, C]]) -> Callable[P2, Callable[P1, C]]:
# Identity function! # Identity function!
def ident(x: A) -> A: def ident(x: A) -> A:
"The identity function. Output is identical to input."
return x return x
# Partial Appliaction shorthand # Partial Appliaction shorthand
p = partial p = partial
"An alias for partial application"
# Two and three-argument currying # Two and three-argument currying
# Defining these pointfree fucks up the types btw # Defining these pointfree fucks up the types btw
def cur2(f: Callable[Concatenate[A, P], C]) -> Callable[[A], Callable[P, C]]: def cur2(f: Callable[Concatenate[A, P], C]) -> Callable[[A], Callable[P, C]]:
"""
Perform two-argument currying.
For example, a function from (A, B) -> C becomes a function A -> B -> C. This can
also be though of as simply moving the first argument of a function out front, since
it preserves any arguments after the first. That is, a function (A, B, C, kw=D) -> E
becomes the function A -> (B, C, kw=D) -> E after being curried using this function.
Can also be used as an annotation.
"""
return p(p, f) #type:ignore return p(p, f) #type:ignore
def cur3(f: Callable[Concatenate[A, B, P], D]) -> Callable[[A], Callable[[B], Callable[P, D]]]: def cur3(f: Callable[Concatenate[A, B, P], D]) -> Callable[[A], Callable[[B], Callable[P, D]]]:
"""
Perform three-argument currying.
See `cur2` for an explaination of how this works.
"""
return p(p, p, f) #type:ignore return p(p, p, f) #type:ignore
# Curried versions of map & filter with stricter types # Curried versions of map & filter with stricter types
def p_map(f: Callable[[A], B]) -> Callable[[Sequence[A]], Sequence[B]]: def p_map(f: Callable[[A], B]) -> Callable[[Sequence[A]], Sequence[B]]:
"A curried version of the built in `map` function"
return partial(map, f) #type: ignore return partial(map, f) #type: ignore
def p_filter(f: Callable[[A], bool]) -> Callable[[Sequence[A]], Sequence[A]]: def p_filter(f: Callable[[A], bool]) -> Callable[[Sequence[A]], Sequence[A]]:
"A curried version of the built in `filter` function"
return partial(filter,f) #type: ignore return partial(filter,f) #type: ignore
# Normal Accessors # Normal Accessors
@cur2 @cur2
def indx(i: int, s: Sequence[A]) -> A: def indx(i: int, s: Sequence[A]) -> A:
"A curried version of the getitem function"
return s[i] return s[i]
fst = indx(0) fst = indx(0)
"Get the first element of a tuple/sequence"
snd = indx(1) snd = indx(1)
"Get the second element of a tuple/sequence"
# Semantic Editor Combinators # Semantic Editor Combinators
class SemEdComb: class SemEdComb:
"""
A tool which approximates semantic editor combinators in python.
Please read
https://web.archive.org/web/20221202200001/http://conal.net/blog/posts/semantic-editor-combinators
for context.
Since Python has no infix function composition, using this pattern can get pretty
ugly. This class abuses python's ability to override the property accessor (.) in
order to approximate semantic editor combinators.
Using this class, you can write `result.first.pmap(reverse)(myList)` to perform an
effect equivalent to `result . first reverse myList` in haskell.
Unfortunately, due to limitations of Python's type system, this class is largely
untyped.
"""
class Inner(): class Inner():
"A chain of semantic editor combinators already paired with a map function"
def __init__(self, f: Callable, name: str): def __init__(self, f: Callable, name: str):
self.f = f self.f = f
self.name = name self.name = name
def and_then(self, other: 'SemEdComb.Inner') -> 'SemEdComb.Inner': def and_then(self, other: 'SemEdComb.Inner') -> 'SemEdComb.Inner':
"Composes this with another `SemEdComb.Inner`"
return SemEdComb.Inner(c(other.f, self.f), self.name + ' and ' + other.name) return SemEdComb.Inner(c(other.f, self.f), self.name + ' and ' + other.name)
def __repr__(self) -> str: def __repr__(self) -> str:
return f"SemEdComb*({self.name})" return f"SemEdComb*({self.name})"
@ -78,18 +135,25 @@ class SemEdComb:
return SemEdComb(c(self.f, next_f), self.name + next_fname) return SemEdComb(c(self.f, next_f), self.name + next_fname)
RESULT = cur2(c) RESULT = cur2(c)
"Map the result of a function"
ARG = flip(RESULT) ARG = flip(RESULT)
"Map the argument of a function"
ALL = p_map ALL = p_map
"Map every element of a list"
@cur3 @cur3
@staticmethod @staticmethod
def INDEX(i, f, arr): def INDEX(i, f, arr):
"Map the ith element of a mutable sequence"
arr[i] = f(arr[i]) arr[i] = f(arr[i])
return arr return arr
@cur3 @cur3
@staticmethod @staticmethod
def INDEX_TUP(i: int, f: Callable[[Any], Any], tup: Tuple) -> Tuple: def INDEX_TUP(i: int, f: Callable[[Any], Any], tup: Tuple) -> Tuple:
"Map the ith element of an immutable sequence"
l = list(tup) l = list(tup)
l[i] = f(l[i]) l[i] = f(l[i])
return (*l,) return (*l,)
@ -97,51 +161,69 @@ class SemEdComb:
@cur2 @cur2
@staticmethod @staticmethod
def FIRST(f: Callable[[A], C], tup: Tuple[A, B]) -> Tuple[C, B]: def FIRST(f: Callable[[A], C], tup: Tuple[A, B]) -> Tuple[C, B]:
"Map the first element of a two-tuple"
return (f(tup[0]), tup[1]) return (f(tup[0]), tup[1])
@cur2 @cur2
@staticmethod @staticmethod
def SECOND(f: Callable[[B], C], tup: Tuple[A, B]) -> Tuple[A, C]: def SECOND(f: Callable[[B], C], tup: Tuple[A, B]) -> Tuple[A, C]:
"Map the second element of a two-tuple"
return (tup[0], f(tup[1])) return (tup[0], f(tup[1]))
@property @property
def result(self) -> 'SemEdComb': def result(self) -> 'SemEdComb':
"Map the result of a function"
return self._c(SemEdComb.RESULT, '.result') return self._c(SemEdComb.RESULT, '.result')
@property @property
def arg(self) -> 'SemEdComb': def arg(self) -> 'SemEdComb':
"Map the argument of a function"
return self._c(SemEdComb.ARG, '.arg') return self._c(SemEdComb.ARG, '.arg')
@property @property
def all(self) -> 'SemEdComb': def all(self) -> 'SemEdComb':
"Map every element of a list"
return self._c(SemEdComb.ALL, '.all') return self._c(SemEdComb.ALL, '.all')
def index(self, i) -> 'SemEdComb': def index(self, i) -> 'SemEdComb':
"Map the ith element of a mutable sequence"
return self._c(SemEdComb.INDEX(i), f'.index({i})') return self._c(SemEdComb.INDEX(i), f'.index({i})')
def index_tup(self, i) -> 'SemEdComb': def index_tup(self, i) -> 'SemEdComb':
"Map the ith element of an immutable sequence"
return self._c(SemEdComb.INDEX_TUP(i), f'.index_tup({i})') return self._c(SemEdComb.INDEX_TUP(i), f'.index_tup({i})')
@property @property
def first(self) -> 'SemEdComb': def first(self) -> 'SemEdComb':
"Map the first element of a two-tuple"
return self._c(SemEdComb.FIRST, f'.first') return self._c(SemEdComb.FIRST, f'.first')
@property @property
def second(self) -> 'SemEdComb': def second(self) -> 'SemEdComb':
"Map the second element of a two-tuple"
return self._c(SemEdComb.SECOND, f'.second') return self._c(SemEdComb.SECOND, f'.second')
def __repr__(self): def __repr__(self):
return f"SemEdComb({self.name})" return f"SemEdComb({self.name})"
def pmap(self, mapper): def pmap(self, mapper):
"""
Set the mapper function, but don't call it yet
The name is short for partial map.
See also: `map`
"""
return SemEdComb.Inner(self.f(mapper), self.name) return SemEdComb.Inner(self.f(mapper), self.name)
def map(self, mapper, thing_to_map) -> Callable: def map(self, mapper, thing_to_map) -> Callable:
"Apply the chain of combinators to a mapper and a mappee"
return self.pmap(mapper)(thing_to_map) return self.pmap(mapper)(thing_to_map)
def __call__(self, *args, **kwargs): def __call__(self, *args, **kwargs):
return self.f(*args, **kwargs) return self.f(*args, **kwargs)
# Pre-constructed base semantic editor combinators
result = SemEdComb(SemEdComb.RESULT, 'result') result = SemEdComb(SemEdComb.RESULT, 'result')
arg = SemEdComb(SemEdComb.ARG, 'arg') arg = SemEdComb(SemEdComb.ARG, 'arg')
index = lambda i: SemEdComb(SemEdComb.INDEX(i), f'index({i})') index = lambda i: SemEdComb(SemEdComb.INDEX(i), f'index({i})')
@ -153,15 +235,36 @@ all_ = SemEdComb(SemEdComb.ALL, 'all')
# Tail call optimizing recursion # Tail call optimizing recursion
@dataclass @dataclass
class Recur(Generic[P]): class Recur(Generic[P]):
"""
Indicate that the function this is returned from should be called again with new args.
Exclusively used with `tco_rec()`
"""
def __init__(self, *args: P.args, **kwargs: P.kwargs): def __init__(self, *args: P.args, **kwargs: P.kwargs):
self.args = args self.args = args
self.kwargs = kwargs self.kwargs = kwargs
@dataclass(frozen = True) @dataclass(frozen = True)
class Return(Generic[B]): class Return(Generic[B]):
"""
Indicate that the function this is returned from should return this value
Exclusively used with `tco_rec()`
"""
val: B val: B
def tco_rec(f: Callable[P, Recur[P] | Return[B]]) -> Callable[P, B]: def tco_rec(f: Callable[P, Recur[P] | Return[B]]) -> Callable[P, B]:
"""
Run a tail-recursive function in a mannor which will not overflow the stack.
Wraps a function in a loop which transforms its return type. The function is expected
to return an instance of `Recur` rather than calling itself to recur. The arguments
passed to the returned `Recur` instance become the arguments to the next iteration of
the function call. When the function is ready to return for real, it should return an
instance of `Return`.
The function will be transformed by `tco_rec` to look as if it is a normal function.
"""
@wraps(f) @wraps(f)
def tco_loop(*args: P.args, **kwargs: P.kwargs) -> B: def tco_loop(*args: P.args, **kwargs: P.kwargs) -> B:
while True: while True:
@ -175,21 +278,34 @@ def tco_rec(f: Callable[P, Recur[P] | Return[B]]) -> Callable[P, B]:
# Options! # Options!
@dataclass(frozen=True) @dataclass(frozen=True)
class Some(Generic[A]): class Some(Generic[A]):
"""
The positive part of an optional datatype
Component of `Option` and counterpart of `None`
"""
val: A val: A
Option = Some[A] | None Option = Some[A] | None
"An Option datatype, aka Maybe"
def map_opt(f: Callable[[A], B], o: Option[A]) -> Option[B]: def map_opt(f: Callable[[A], B], o: Option[A]) -> Option[B]:
"""
Map the contents of an optional data type. Has no effect on `None`
"""
match o: match o:
case Some(val): case Some(val):
return Some(f(val)) return Some(f(val))
case none: case none:
return none return none
def bind_opt(f: Callable[[A], Option[B]], o: Option[A]) -> Option[B]: def bind_opt(f: Callable[[A], Option[B]], o: Option[A]) -> Option[B]:
"wow! monads! (aka 'and_then')"
match o: match o:
case Some(val): case Some(val):
return f(val) return f(val)
case none: case none:
return none return none
def note(e: B, o: Option[A]) -> Result[A, B]: def note(e: B, o: Option[A]) -> 'Result[A, B]':
"Convert an `Option` to a `Result` by attaching an error to the `None` variants"
match o: match o:
case Some(val): case Some(val):
return Ok(val) return Ok(val)
@ -199,54 +315,95 @@ def note(e: B, o: Option[A]) -> Result[A, B]:
# Results! # Results!
@dataclass(frozen=True) @dataclass(frozen=True)
class Ok(Generic[A]): class Ok(Generic[A]):
"""
The positive part of a result (either) datatype
Component of `Result` and counterpart of `Err`
"""
val: A val: A
@dataclass(frozen=True) @dataclass(frozen=True)
class Err(Generic[B]): class Err(Generic[B]):
"""
The error part of a result (either) datatype
Component of `Result` and counterpart of `Ok`
"""
err: B err: B
def __bool__(self): def __bool__(self):
return False return False
Result = Ok[A] | Err[B] Result = Ok[A] | Err[B]
"A Result datatype, aka Either"
def map_res(f: Callable[[A], C], r: Result[A, B]) -> Result[C, B]: def map_res(f: Callable[[A], C], r: Result[A, B]) -> Result[C, B]:
"Map the success value of a result"
match r: match r:
case Ok(val): case Ok(val):
return Ok(f(val)) return Ok(f(val))
case not_okay: case not_okay:
return not_okay return not_okay
def bind_res(f: Callable[[A], Result[C, B]], r: Result[A, B]) -> Result[C, B]: def bind_res(f: Callable[[A], Result[C, B]], r: Result[A, B]) -> Result[C, B]:
"Perform an fallible operation for successful results."
match r: match r:
case Ok(val): case Ok(val):
return f(val) return f(val)
case not_okay: case not_okay:
return not_okay return not_okay
def map_err(f: Callable[[B], C], r: Result[A, B]) -> Result[A, C]: def map_err(f: Callable[[B], C], r: Result[A, B]) -> Result[A, C]:
"Map the error value of a result"
match r: match r:
case Err(e): case Err(e):
return Err(f(e)) return Err(f(e))
case oki_doke: case oki_doke:
return oki_doke return oki_doke
def hush(r: Result[A, Any]) -> Option[A]: def hush(r: Result[A, Any]) -> Option[A]:
"Convert a `Result` to an `Option` by converting any errors to `None`"
match r: match r:
case Ok(val): case Ok(val):
return Some(val) return Some(val)
case not_okay: case not_okay:
return None return None
def try_(handle: Callable[[Exception], B], f: Callable[P, A], *args: P.args, **kwargs: P.kwargs) -> Result[A, B]: def try_(handle: Callable[[Exception], B], f: Callable[P, A], *args: P.args, **kwargs: P.kwargs) -> Result[A, B]:
"""
Try-catch in a function! Attempt to perform and operation, and `Err` on failure
Arguments:
handle - A function which handles any exceptions which arise. The return type is
what will be wrapped into the resulting `Err`. This is not called if nothing
goes wrong.
f - The fallible function to try. If this succeeds without raising an error, that
value is returned in an `Ok`. If this raises an exception, that exception
will be passed to `handle`.
args - Will be passed to `f` when it is called.
kwargs - Will be passed to `f` when it is called.
"""
try: try:
return Ok(f(*args, **kwargs)) return Ok(f(*args, **kwargs))
except Exception as e: except Exception as e:
return Err(handle(e)) return Err(handle(e))
def unwrap_r(r: Result[A, Any]) -> A: def unwrap_r(r: Result[A, Any]) -> A:
"""
Assert that a `Result` is `Ok` and return it's value.
Throws:
`AssertionError` - The result was NOT okay. The `AssertionError` will have two
arguments: The first is a string to make it more obvious what happened. The
second is the error that was stored in the `Err`.
"""
match r: match r:
case Ok(val): case Ok(val):
return val return val
case Err(e): case Err(e):
raise Exception(f'Tried to unwrap an error: {e}') raise AssertionError(f'Tried to unwrap an error: ', e)
def sequence(s: Sequence[Result[A, B]]) -> Result[Iterator[A], B]: def sequence(s: Sequence[Result[A, B]]) -> Result[Iterator[A], B]:
"""
Convert a list of results into a result of a list.
If the input sequence contains only `Ok` results, then the output is similarly `Ok`,
and contains a list of all the unwrapped values of the `Ok`s. If there are any
errors, proccessing of the sequence is immediately stopped, and the first error
encountered is returned.
"""
if all(s): if all(s):
return Ok(( return Ok(map(unwrap_r, s))
unwrap_r(r)
for r in s
))
else: else:
o = next(filter(not_, s)) o = next(filter(not_, s))
assert isinstance(o, Err) assert isinstance(o, Err)