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.

class FuPy.basics.Empty(*args, **kwargs)[source]

The Empty type, without any values.

class FuPy.basics.Unit[source]

The Unit type 𝟙 with only the unit value.

A singleton class.

FuPy.basics.unit: Unit

The single value in Unit.

FuPy.basics.Either[A, B]

The (disjoint) sum type A + B.

FuPy.basics.left(a: A) Either[source]

Inject left into sum type. Prints as ‘↪︎’.

FuPy.basics.right(b: B) Either[source]

Inject right into sum type. Prints as ‘↩︎’.

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.

FuPy.basics.first(t: Both) A[source]

Project left out of product type. Prints as ‘≪’.

FuPy.basics.second(t: Both) B[source]

Project right out of product type. Prints as ‘≫’.

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:

    1. args[0] must behave like an empty tuple, e.g. unit.

    1. pack the arguments and pas them as a single tuple.

    1. unpack the argument and pass the unpacked values separately; N.B. the single argument must be an iterable of appropriate length

    1. 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.undefined(a: A = unit) Empty[source]

Abort evaluation. Prints as ‘⊥’.

FuPy.basics.id_(a: A) A[source]

Polymorphic identity function. Prints as ‘id’.

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.curry(f: Func[Both, C]) Func[A, Func][source]

Curry f.

FuPy.basics.uncurry(f: Func[A, Func]) Func[Both, C][source]

Uncurry f.

FuPy.basics.flip(f: Func[A, Func]) Func[B, Func][source]

Flip f: swap arguments on curried function f.

FuPy.basics.ap(f: Func, x: A) B[source]

Function application as uncurried operator.

FuPy.basics.compose(f: Func, g: Func) Func[source]

Function composition as uncurried operator.

FuPy.basics.split(f: Func, g: Func) Func[A, Both][source]

Function split as uncurried operator.

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.

FuPy.basics.fpower(f: Func, n: int) Func[source]

Repeated function composition. This is fold(-right) over nat. Also see FuPy.prelude.cata_nat().

Assumption: n >= 0

FuPy.basics.fpower_left(f: Func, n: int) Func[source]

Repeated function composition. This is like fold-left over nat. It is tail recursive and was turned into a loop.

Assumption: n >= 0