Basics
Definitions to support functional programming in Python. Especially aimed at educational use and not at industrial application.
Copyright (c) 2024 - Eindhoven University of Technology, The Netherlands
This software is made available under the terms of the MIT License.
- FuPy.basics.Either[A, B]
The (disjoint) sum type A + B.
- FuPy.basics.guard(p: Func[A, bool]) Func[A, Either][source]
For predicate p, construct function p? in (A -> bool) -> A -> Either[A, A]. Prints as ‘(?)’.
- FuPy.basics.Both[A, B]
The (Cartesian) product type A ⨯ B.
- class FuPy.basics.Func(f: Callable[[...], B], name: str | None = None, r: int | None = None, top: str = '')[source]
Function space type of functions A -> B.
Supports various operations on such functions:
application (f(arg)), composition (f @ g)
split (f & g), case (f | g)
product (f * g), sum (f + g)
exponentiation (f ** n)
These operators can be sectioned.
It would be nice if this could be incorporated into types.FunctionType (which is immutable; so, these cannot be added dynamically.)
Note that in an FP language like Haskell, functions take exactly one argument (and return one result). That is why we give a Func the unit value as default argument. There is some tension between this view and the broader view of Python on functions (the latter can have no arguments or multiple arguments). The Func wrapper handles this by automatically (un)packing of arguments. Consider the call Func(f)(*args) where args is a tuple and f is defined as def f(*fargs): … where fargs is also a tuple. Let R be the number of _required_ non-keyword arguments of f and A = len(args) the number of actual arguments of Func(f). The call Func(f)(*args) is handled as follows:
A == 0
A == 1
A >= 2
R == 0
f(*args)
f(*(args[0])) (a)
TypeError
R == 1
f(unit)
f(args[0])
f(args) (b)
R >= 2
TypeError
f(*(args[0])) (c)
f(*args) (d)
Notes:
args[0] must behave like an empty tuple, e.g. unit.
pack the arguments and pas them as a single tuple.
unpack the argument and pass the unpacked values separately; N.B. the single argument must be an iterable of appropriate length
the number of arguments provided and expected must match.
Keyword parameters are passed on.
- FuPy.basics.func(obj: Callable | None = None, **kwargs) Callable[source]
Decorate obj by wrapping it in Func.
When obj is None, it returns a wrapping function. Avoids double wrapping.
- FuPy.basics.la(la_expr: str, up: int = 1) Func[Any, Any][source]
Define a lambda abstraction with zero or more bound variables. Returns a curried or uncurried function wrapped in Func. Capture local and global variables up call stack frames up.
Argument la_expr must have the shape:
variables: expr
where variables is a comma-separated list: variable, …, variable, where each variable can be * empty (no name/tuple) * a single name * a tuple of zero or more names
Empty or an empty tuple results in a constant function with unit as argument. A tuple of 2 or more names results in an uncurried function. Multiple variables result in a curried function.
la(‘a, (b, c), , d: (a, b, c, d, e, f)’) where e == 42 locally and f is global, results (roughly) in:
Func(lambda a: Func(lambda b, c: # this is an uncurried function Func(lambda: # this is a constant function Func(lambda d: (a, b, c, d, e), name=...), name=...), name=...), name=...)
Recursive approach, with auto capturing of locals.
- FuPy.basics.const(b: B) Func[source]
Construct the constant function always returning b. Prints as ‘(°)’. We use the (superscript) degree symbol as best approximation of a superscript bullet (which is not available in Unicode).
- class FuPy.basics.OperatorSection[source]
Support for operator sectioning.
OperatorSection() is used as dummy argument to implicitly create lambda expressions. It must not implement __call__, because otherwise an OperatorSection object will not work with Func function combinators as expected.
- FuPy.basics.flip(f: Func[A, Func]) Func[B, Func][source]
Flip f: swap arguments on curried function f.
- FuPy.basics.case_(f: Func, g: Func) Func[Either, C][source]
Function case as uncurried operator. Prints as ‘case’.
- FuPy.basics.ftimes(f: Func, g: Func) Func[Both, Both][source]
Functorial product as uncurried operator.
- FuPy.basics.fplus(f: Func, g: Func) Func[Either, Either][source]
Functorial sum as uncurried operator.