Add in some doctests

This commit is contained in:
Emi Simpson 2023-02-10 16:33:54 -05:00
parent a5446551c2
commit 1c3558828d
Signed by: Emi
GPG Key ID: A12F2C2FFDC3D847
1 changed files with 306 additions and 18 deletions

View File

@ -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
return o
if __name__ == '__main__':
import doctest
doctest.testmod()