FuPy logo # Implementation Details The main classes in `FuPy` are * {class}`FuPy.basics.Func` * {class}`FuPy.basics.OperatorSection` * {class}`FuPy.laziness.Lazy` For evaluation tracing there are also * {class}`FuPy.tracing.BaseStep` * {class}`FuPy.tracing.Trace` The classes {class}`FuPy.basics.Empty` and {class}`FuPy.basics.Unit` are just used as specific types (empty and singleton, respectively). ## `Func` Generic class {class}`FuPy.basics.Func[A, B]` serves two roles: * `Func[A, B]` is the function space type for functions from `A` to `B`, also written as `A -> B`. * It is a (thin) wrapper for function objects (or other callables), so that function combinators are supported as infix operators, by overloading `@` (composition), `|` (case), `&` (split), `+` (functorial plus), `*` (functorial times), `**` (exponentiation). The underlying function is stored in the attribute `self.func`. * This wrapper also carries some additional information in attributes: - `name`: how the function prints, in Math notation - `top`: the top-level operator, in Math notation, if the function is an expression, and `''` otherwise - `required_args_count`: the minimum number of required arguments of `self.func`; this is used for auto-(un)packing of arguments ## `OperatorSection` Class {class}`FuPy.basics.OperatorSection` is a helper class to make operator sections work. Only one instance of this class is needed and it is made available as `x_` (though it is recommended to rename it to `_`). `OperatorSection` redefines various overloadable infix operators `op` (such as '@'), so that * `(_ op other)`, * `(other op _)`, and * `(_ op _)`, to behave as the corresponding operator sections * `Func(lambda x: x op other)` * `Func(lambda x: other op x)` * `Func(lambda x: Func(lambda y: x op y)` ## `Lazy` TO BE COMPLETED ## Interactions between `Func`, `OperatorSection`, `Lazy` Suppose that * `f` and `g` are instances of `Func` * `_` is an instance of `OperatorSection` * `theta` and `theta_` are instances of `Lazy` Here is how their various combinations under `@` are handled. * `f @ g`: `f.__matmul__(g)` returns `Func(lambda x: f(g(x)))` * `f @ _`: `f.__matmul__(_)` returns `NotImplemented`; next `_.__rmatmul__(f)` returns `Func(lambda x: f @ x)` * `f @ theta`: `f.__matmul__(theta)` returns `NotImplemented`; next `theta.__rmatmul__(f)` evaluates `theta`, say to value `g` (which must be of type `Func`) and invokes `f @ g`, which (see above)) is handled by `f.__matmul__(g)` * `_ @ f`: `_.__matmul__(f) returns `Func(lambda x: x @ f)` * `_ @ _`: `_.__matmul__(_) returns `Func(lambda x: x @ _)`; when the latter is called, say on `f`, this results in `f @ _` (see above) * `_ @ theta`: `_.__matmul__(theta) returns `Func(lambda x: x @ theta)`; when the latter is called, say on `f`, this results in `f @ theta` (see above) * `theta @ g`: `theta.__matmul__(g)` evaluates `theta`, say to value `f` (which must be of type `Func`), and invokes `f @ g`, which is handled by `f.__matmul__(g)` * `theta @ _`: `theta.__matmul__` returns `NotImplemented`; next `_.__rmatmul__(theta)` returns `Func(lambda x: theta @ x)`; when the latter is called, say on `g`, this results in `theta @ g` (see above) * `theta @ theta_`: `theta.__matmul__(theta_)` evaluates `theta`, say to the value `f` (which must be of type `Func`) and invokes `f @ theta_` (see above) Combinators `|` (case) and `&` (split) are handled similarly. Combinators `+` (functorial plus) and `*` (functorial times) are different, because these operators are also meaningful for types other than `Func`, in particular numbers and strings. Combinator `**`