From a5446551c247e2559bc4c5a52624344378a329ae Mon Sep 17 00:00:00 2001 From: Emi Simpson Date: Fri, 10 Feb 2023 16:33:00 -0500 Subject: [PATCH 1/2] Add imports --- emis_funky_funktions.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/emis_funky_funktions.py b/emis_funky_funktions.py index 018c3fa..318c2c3 100644 --- a/emis_funky_funktions.py +++ b/emis_funky_funktions.py @@ -1,7 +1,7 @@ from dataclasses import dataclass from functools import partial, wraps from operator import not_ -from typing import Any, Callable, Concatenate, Generic, Iterator, ParamSpec, Sequence, Tuple, TypeVar +from typing import Any, Callable, Concatenate, Generic, Iterable, Iterator, List, ParamSpec, Sequence, Tuple, TypeVar A = TypeVar('A') B = TypeVar('B') From 1c3558828d1e267ea2115540edc8a3d8a3ed03a2 Mon Sep 17 00:00:00 2001 From: Emi Simpson Date: Fri, 10 Feb 2023 16:33:54 -0500 Subject: [PATCH 2/2] Add in some doctests --- emis_funky_funktions.py | 324 +++++++++++++++++++++++++++++++++++++--- 1 file changed, 306 insertions(+), 18 deletions(-) diff --git a/emis_funky_funktions.py b/emis_funky_funktions.py index 318c2c3..4e5f0ae 100644 --- a/emis_funky_funktions.py +++ b/emis_funky_funktions.py @@ -21,6 +21,14 @@ def c(f2: Callable[[B], C], f1: Callable[P, B]) -> Callable[P, C]: This can also be thought of as mapping the output of a function using the first parameter as a mapper function. + + >>> double = lambda x: x + x + >>> succ = lambda x: x + 1 + >>> c(double, succ)(1) + 4 + + >>> c(succ, double)(1) + 3 """ @wraps(f1) def inner(*args: P.args, **kwargs: P.kwargs) -> C: @@ -34,6 +42,13 @@ def flip(f: Callable[P1, Callable[P2, C]]) -> Callable[P2, Callable[P1, C]]: This only works with curried functions, so apply `cur2` or `cur3` before applying `flip` if the arguments you want to flip are not curried. + + >>> pair = lambda x: lambda y: (x, y) + >>> pair(1)(2) + (1, 2) + + >>> flip(pair)(1)(2) + (2, 1) """ @wraps(f) def inner1(*args2: P2.args, **kwargs2: P2.kwargs) -> Callable[P1, C]: @@ -45,7 +60,15 @@ def flip(f: Callable[P1, Callable[P2, C]]) -> Callable[P2, Callable[P1, C]]: # Identity function! def ident(x: A) -> A: - "The identity function. Output is identical to input." + """ + The identity function. Output is identical to input. + + >>> ident(3) + 3 + + >>> ident(('hello', 8)) + ('hello', 8) + """ return x def replace(replace_with: A) -> Callable[..., A]: @@ -55,6 +78,16 @@ def replace(replace_with: A) -> Callable[..., A]: The argument `replace_with` is the value the the returned function should always return. The returned function can be used as if having any arity, and will always return the same value originally passed to `replace`. + + >>> always_seven = replace(7) + >>> always_seven(2) + 7 + + >>> always_seven('hello', 'world!') + 7 + + >>> replace('uwu')('NYA!') + 'uwu' """ def constant(*args: Any, **kwargs: Any) -> A: "Always return a constant value, typically the one passed to `replace`" @@ -77,6 +110,21 @@ def cur2(f: Callable[Concatenate[A, P], C]) -> Callable[[A], Callable[P, C]]: becomes the function A -> (B, C, kw=D) -> E after being curried using this function. Can also be used as an annotation. + + >>> @cur2 + ... def pair(x, y): + ... return (x, y) + ... + >>> pair(1)(2) + (1, 2) + + >>> alternate_pair = lambda x, y: (x, y) + >>> cur2(alternate_pair)(1)(2) + (1, 2) + + >>> threeple = lambda x, y, z: (x, y, z) + >>> cur2(threeple)(1)(2, 3) + (1, 2, 3) """ return p(p, f) #type:ignore def cur3(f: Callable[Concatenate[A, B, P], D]) -> Callable[[A], Callable[[B], Callable[P, D]]]: @@ -84,9 +132,14 @@ def cur3(f: Callable[Concatenate[A, B, P], D]) -> Callable[[A], Callable[[B], Ca Perform three-argument currying. See `cur2` for an explaination of how this works. + + >>> threeple = lambda x, y, z: (x, y, z) + >>> cur3(threeple)(1)(2)(3) + (1, 2, 3) """ return p(p, p, f) #type:ignore + # Curried versions of map & filter with stricter types def p_map(f: Callable[[A], B]) -> Callable[[Sequence[A]], Sequence[B]]: "A curried version of the built in `map` function" @@ -99,7 +152,16 @@ def p_filter(f: Callable[[A], bool]) -> Callable[[Sequence[A]], Sequence[A]]: # Normal Accessors @cur2 def indx(i: int, s: Sequence[A]) -> A: - "A curried version of the getitem function" + """ + A curried version of the getitem function + + >>> get_second = indx(1) + >>> get_second(('a', 'b')) + 'b' + + >>> get_second([1, 2, 3, 4]) + 2 + """ return s[i] fst = indx(0) @@ -121,8 +183,17 @@ class SemEdComb: 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. + >>> my_func = lambda x: ('abc' + x, 'def') + >>> my_func('hi') + ('abchi', 'def') + + >>> altered_func = result.first.map(str.upper, my_func) + >>> altered_func('hi') + ('ABCHI', 'def') + + >>> other_altered_func = arg.map(str.upper, my_func) + >>> other_altered_func('hello') + ('abcHELLO', 'def') Unfortunately, due to limitations of Python's type system, this class is largely untyped. @@ -185,35 +256,135 @@ class SemEdComb: @property def result(self) -> 'SemEdComb': - "Map the result of a function" + """ + Map the result of a function + + >>> my_func = lambda s: s + ' backwards is ' + s[::-1] + >>> my_func('hello') + 'hello backwards is olleh' + + >>> altered_func = result.map(str.upper, my_func) + >>> altered_func('hello') + 'HELLO BACKWARDS IS OLLEH' + + Can be chained in order to work with curried functions as well. That is, the + result of a two argument curried function is the result of the result of that + function. + + >>> curried_pair = lambda x: lambda y: (x, y) + >>> altered_pair = result.result.second.map(str.upper, curried_pair) + >>> altered_pair('hello')('world') + ('hello', 'WORLD') + """ return self._c(SemEdComb.RESULT, '.result') @property def arg(self) -> 'SemEdComb': - "Map the argument of a function" + """ + Map the argument of a function + + >>> my_func = lambda s: s + ' backwards is ' + s[::-1] + >>> my_func('hello') + 'hello backwards is olleh' + + >>> altered_func = arg.map(str.upper, my_func) + >>> altered_func('hello') + 'HELLO backwards is OLLEH' + + Can be combined with `.result` to work with curried functions. + + >>> curried_pair = lambda x: lambda y: (x, y) + >>> altered_pair = result.arg.map(str.upper, curried_pair) + >>> altered_pair('hello')('world') + ('hello', 'WORLD') + """ return self._c(SemEdComb.ARG, '.arg') @property def all(self) -> 'SemEdComb': - "Map every element of a list" + """ + Map every element of a sequence + + To use this as the base of a chain of SECs, write "all_", since "all" by itself + refers to the builtin python function, which is different. + + Note that this returns an iterator, not a sequence, even if the thing being mapped + was a sequence or a list. + + >>> list(all_.map(lambda x: x + x, [1, 2, 3])) + [2, 4, 6] + + >>> my_func = lambda s: [s] * s + >>> my_func(3) + [3, 3, 3] + + >>> altered_func = result.all.map(lambda x: x + x, my_func) + >>> list(altered_func(3)) + [6, 6, 6] + """ return self._c(SemEdComb.ALL, '.all') def index(self, i) -> 'SemEdComb': - "Map the ith element of a mutable sequence" + """ + Map the ith element of a mutable sequence + + >>> index(1).map(lambda x: x + x, [1, 2, 3]) + [1, 4, 3] + + >>> my_func = lambda s: [s] * s + >>> my_func(3) + [3, 3, 3] + + >>> altered_func = result.index(1).map(lambda x: x + x, my_func) + >>> list(altered_func(3)) + [3, 6, 3] + + """ return self._c(SemEdComb.INDEX(i), f'.index({i})') def index_tup(self, i) -> 'SemEdComb': - "Map the ith element of an immutable sequence" + """ + Map the ith element of an immutable sequence. + + >>> index_tup(2).map(lambda x: x + x, (1, 2, 3, 4)) + (1, 2, 6, 4) + + See Also: `index` + + For a more optimized version of this method specialized to two-tuples, see `first` + and `second` + """ return self._c(SemEdComb.INDEX_TUP(i), f'.index_tup({i})') @property def first(self) -> 'SemEdComb': - "Map the first element of a two-tuple" + """ + Map the first element of a two-tuple + + >>> first.map(lambda x: x+x, (1, 2)) + (2, 2) + + Doesn't work for threeples and fourples. If this is the behaviour you need, try + `index_tup` + + >>> first.map(lambda x: x+x, (1, 2, 3)) + (2, 2) + """ return self._c(SemEdComb.FIRST, f'.first') @property def second(self) -> 'SemEdComb': - "Map the second element of a two-tuple" + """ + Map the second element of a two-tuple + + >>> second.map(lambda x: x+x, (1, 2)) + (1, 4) + + As with `first`, this doesn't work with threeples, fourples, and moreples. + + >>> second.map(lambda x: x+x, (1, 2, 3)) + (1, 4) + """ return self._c(SemEdComb.SECOND, f'.second') def __repr__(self): @@ -225,6 +396,15 @@ class SemEdComb: The name is short for partial map. + >>> my_func = lambda s1: lambda s2: f"You entered {s1} and the pair {s2}" + >>> my_func(1)(('hello', 'world')) + "You entered 1 and the pair ('hello', 'world')" + + >>> mapper = result.arg.first.pmap(str.upper) + >>> altered_func = mapper(my_func) + >>> altered_func(1)(('hello', 'world')) + "You entered 1 and the pair ('HELLO', 'world')" + See also: `map` """ return SemEdComb.Inner(self.f(mapper), self.name) @@ -277,6 +457,15 @@ def tco_rec(f: Callable[P, Recur[P] | Return[B]]) -> Callable[P, B]: instance of `Return`. The function will be transformed by `tco_rec` to look as if it is a normal function. + + >>> @tco_rec + ... def factorial(n, coefficient = 1): + ... if n > 1: + ... return Recur(n - 1, coefficient * n) + ... else: + ... return Return(coefficient) + >>> factorial(4) + 24 """ @wraps(f) def tco_loop(*args: P.args, **kwargs: P.kwargs) -> B: @@ -297,6 +486,8 @@ class Some(Generic[A]): Component of `Option` and counterpart of `None` """ val: A + def __repr__(self) -> str: + return f'Some({self.val!r})' Option = Some[A] | None "An Option datatype, aka Maybe" @@ -304,6 +495,12 @@ Option = Some[A] | None 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` + + >>> map_opt(str.upper, Some('hello')) + Some('HELLO') + + >>> map_opt(str.upper, None) is None + True """ match o: case Some(val): @@ -311,14 +508,37 @@ def map_opt(f: Callable[[A], B], o: Option[A]) -> Option[B]: case none: return none def bind_opt(f: Callable[[A], Option[B]], o: Option[A]) -> Option[B]: - "wow! monads! (aka 'and_then')" + """ + wow! monads! (aka 'and_then') + + >>> halve = lambda n: Some(n//2) if n % 2 == 0 else None + >>> [halve(2), halve(3)] + [Some(1), None] + + >>> bind_opt(halve, Some(4)) + Some(2) + + >>> bind_opt(halve, Some(5)) is None + True + + >>> bind_opt(halve, None) is None + True + """ match o: case Some(val): return f(val) case none: return none def note(e: B, o: Option[A]) -> 'Result[A, B]': - "Convert an `Option` to a `Result` by attaching an error to the `None` variants" + """ + Convert an `Option` to a `Result` by attaching an error to the `None` variants + + >>> note('woops!', Some(1)) + Ok(1) + + >>> note('woops!', None) + Err('woops!') + """ match o: case Some(val): return Ok(val) @@ -334,6 +554,8 @@ class Ok(Generic[A]): Component of `Result` and counterpart of `Err` """ val: A + def __repr__(self) -> str: + return f'Ok({self.val!r})' @dataclass(frozen=True) class Err(Generic[B]): """ @@ -342,38 +564,80 @@ class Err(Generic[B]): Component of `Result` and counterpart of `Ok` """ err: B + def __repr__(self) -> str: + return f'Err({self.err!r})' def __bool__(self): return False Result = Ok[A] | Err[B] "A Result datatype, aka Either" def map_res(f: Callable[[A], C], r: Result[A, B]) -> Result[C, B]: - "Map the success value of a result" + """ + Map the success value of a result + + >>> map_res(str.upper, Ok('hai!')) + Ok('HAI!') + + >>> map_res(str.upper, Err('oh noes')) + Err('oh noes') + """ match r: case Ok(val): return Ok(f(val)) case not_okay: return not_okay def bind_res(f: Callable[[A], Result[C, B]], r: Result[A, B]) -> Result[C, B]: - "Perform an fallible operation for successful results." + """ + Perform an fallible operation for successful results. + + >>> halve = lambda n: Ok(n//2) if n % 2 == 0 else Err(f'{n} is not divisible by 2') + >>> [halve(2), halve(3)] + [Ok(1), Err('3 is not divisible by 2')] + + >>> bind_res(halve, Ok(4)) + Ok(2) + + >>> bind_res(halve, Ok(5)) + Err('5 is not divisible by 2') + + >>> bind_res(halve, Err('not okay in the 1st place')) + Err('not okay in the 1st place') + """ match r: case Ok(val): return f(val) case not_okay: return not_okay def map_err(f: Callable[[B], C], r: Result[A, B]) -> Result[A, C]: - "Map the error value of a result" + """ + Map the error value of a result + + >>> map_err(str.upper, Ok('hai!')) + Ok('hai!') + + >>> map_err(str.upper, Err('oh noes')) + Err('OH NOES') + """ match r: case Err(e): return Err(f(e)) case oki_doke: return oki_doke def hush(r: Result[A, Any]) -> Option[A]: - "Convert a `Result` to an `Option` by converting any errors to `None`" + """ + Convert a `Result` to an `Option` by converting any errors to `None` + + >>> hush(Ok('hai!')) + Some('hai!') + + >>> hush(Err('oh noes')) is None + True + """ match r: case Ok(val): return Some(val) case not_okay: return None + 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 @@ -387,11 +651,18 @@ def try_(handle: Callable[[Exception], B], f: Callable[P, A], *args: P.args, **k 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_(ident, int, '3') + Ok(3) + + >>> try_(ident, int, 'three') + Err(ValueError("invalid literal for int() with base 10: 'three'")) """ try: return Ok(f(*args, **kwargs)) except Exception as e: return Err(handle(e)) + def unwrap_r(r: Result[A, Any]) -> A: """ Assert that a `Result` is `Ok` and return it's value. @@ -400,6 +671,13 @@ def unwrap_r(r: Result[A, Any]) -> A: `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`. + + >>> unwrap_r(Ok('hai!')) + 'hai!' + + >>> unwrap_r(Err('oh noes')) is None #doctest: +IGNORE_EXCEPTION_DETAIL + Traceback (most recent call last): + AssertionError: ('Tried to unwrap an error: ', 'oh noes') """ match r: case Ok(val): @@ -414,10 +692,20 @@ def sequence(s: Sequence[Result[A, B]]) -> Result[Iterator[A], B]: 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. + + >>> map_res(list, sequence([Ok(1), Ok(2), Ok(3)])) + Ok([1, 2, 3]) + + >>> sequence([Ok(1), Err('Oops!'), Err('Aw man!')]) + Err('Oops!') """ if all(s): return Ok(map(unwrap_r, s)) else: o = next(filter(not_, s)) assert isinstance(o, Err) - return o \ No newline at end of file + return o + +if __name__ == '__main__': + import doctest + doctest.testmod() \ No newline at end of file