Functions
Functions are first-class values in the Compute Engine. This means that functions can be passed as arguments to other functions, returned from functions, and assigned to variables.
The standard library can be extended with your own functions.
| Term | Definition |
|---|---|
| Function Expression | An expression representing a function application where a function (or operator) is evaluated with arguments (the arguments are applied to the function). For example ["Add", 1, 2] applies the arguments 1 and 2 to the operator Add. |
| Function Signature | A type describing the function's inputs and outputs, e.g. (real, integer) -> boolean |
| Function Literal | A first-class function value, defined using a construct like ["Function", body, params...], which may or may not capture variables from its lexical scope. |
| Shorthand Function Literal | A compact way to write a function literal using placeholders (e.g. _ or _2) instead of explicitly listing parameters, e.g. ["Add", "_", 1] desugared to ["Function", ["Add", "_", 1], "_"]. |
| Closure | A function literal that captures one or more free variables from its defining lexical scope: the function reads the binding of the variable where it was defined, with the value that binding holds when the function is called. The capture happens by simply referencing the variables in the function body. |
| Anonymous Function | A function that is not bound to a symbol, often used as an argument to other functions. |
Function Literals
A function literal is a first-class function value, defined using a
["Function"] expression. It can be passed as an argument to other functions,
returned from a function, or assigned to a variable.
The ["Function"] expression takes a body and a list of parameters.
["Sum", ["Function", ["Multiply", "x", 2], "x"]]
To specify a function literal with LaTeX use the \mapsto command:
["Function", ["Multiply", "x", 2], "x"]
["Function", ["Add", ["Multiply", "x", 2], "y"], "x", "y"]
A parameter may also be a tuple pattern: a ["Tuple", …] of parameter
names (or _ to skip a position, or a nested pattern) in a parameter
position. It is still one parameter — it takes one argument, which must
be a tuple of the pattern's shape, and binds a name to each component. In
Epsil it is written with a second pair of parentheses, ((p, q)) => p + q,
as opposed to the two-parameter (p, q) => p + q.
["Map",
["Function", ["Add", "p", "q"], ["Tuple", "p", "q"]],
["List", ["Tuple", 1, 2], ["Tuple", 3, 4]]]
// ➔ ["List", 3, 7]
An argument that is not a tuple of that shape yields the same
incompatible-type error value a destructuring ["Declare", ["Tuple", …], …]
produces. Function literals with a tuple-pattern parameter are interpreted;
the compile targets decline them rather than emit code that binds the wrong
names.
The examples in this section define functions as a simple expression, but function literals can include more complex control structures, including blocks, local variables, loops and conditionals.
For example, here's a simple "clamp" function, using a ["Block"] expression.
["Function",
["Block",
["Assign", "x", ["Max", "x", "min"]],
["Min", "x", "max"]
],
"x", "min", "max"
]
Shorthand Function Literals
A shorthand function literal is a compact way to write a function literal without explicitly listing parameters.
A shorthand function literal can use either wildcards (_, _2, etc...) or
unknowns (symbols that have no value) as implicit parameters.
The shorthand function literal is desugared to a function literal.
For example the shorthand function literal ["Multiply", "_", 2] is desugared
to ["Function", ["Multiply", "_", 2], "_"].
To use wildcards in LaTeX, they must be wrapped with an \operatorname command except for \_.
["Add", "_", "_2"]
The shorthand function literal ["Multiply", "x", 2] which uses the unknown x
is equivalent to the function literal ["Function", ["Multiply", "x", 2], "x"].
When using this form, make sure that the symbol x is not defined in the current scope.
An expression with no wildcard and no unknown is a constant function:
["Apply", 3, 5] evaluates to 3.
The unknowns of an expression become its parameters only when the expression
is known not to be a function. An expression whose type is unknown is kept
as a function value instead, and is applied once its value is known. This
is the case for a call of a function that has no definition yet: in
["Apply", ["g", "f"], "x"] the callee g(f) is not read as the function
f ↦ g(f), because g(f) may itself be a function. The application stays
["Apply", ["g", "f"], "x"]. Once g has a value, for example
["Function", ["Add", "a", "b"], "a", "b"], it evaluates to f + x: g(f) is
then g applied to its first argument, a function of one argument.
To apply a function with an unknown result type as a shorthand, use a
wildcard: ["Map", ["f", "_"], "xs"] maps f over xs whether or not f
is defined.
A symbol which is the name of an operator (for example Sin) is also a valid
function literal shorthand.
This expression will apply the Sin function to the elements of xs.
["Map", "Sin", "xs"]
It is equivalent to ["Map", ["Sin", "_"], "xs"] which is desugared to
["Map", ["Function", ["Sin", "_"], "_"], "xs"].
Anonymous Functions
A function that is not bound to a symbol is called an anonymous function.
Anonymous functions are frequently used as arguments to other functions.
In the example below, the second argument of the Map function is an
anonymous function expressed as a function literal that multiplies its argument by 2.
["Map", ["Function", ["Multiply", "x", 2], "x"], "xs"]
The same function can be expressed using a shorthand function literal, which
uses _ as a wildcard for the parameter.
["Map", ["Multiply", "_", 2], "xs"]
Evaluating a Function Literal
To apply a function literal to some arguments use an ["Apply"] expression.
["Apply", ["Function", ["Add", 2, "x"], "x"], 11]
// ➔ 13
["Apply", ["Add", 2, "_"], 4]
// ➔ 6
["Apply", "Power", 2, 3]
// ➔ 8
The first argument of Apply is a function literal. The rest of the arguments are the
arguments that will be applied to the function literal.
Broadcasting Over Collections
When a user-defined function with scalar parameter types is applied to a
finite indexed collection, the call is broadcast elementwise: the
function is applied to each element and the results are returned as a
new List.
ce.assign('f', ce.parse('x \\mapsto x^2 + 1'));
ce.expr(['f', ['List', 1, 2, 3]]).evaluate();
// ➔ ["List", 2, 5, 10]
Multi-argument functions broadcast with zip semantics. Scalar arguments are repeated for each element of the collection argument(s):
ce.assign('h', ce.parse('(x, y) \\mapsto x + y'));
// list + scalar
ce.expr(['h', ['List', 1, 2, 3], 10]).evaluate();
// ➔ ["List", 11, 12, 13]
// list + list
ce.expr(['h', ['List', 1, 2, 3], ['List', 10, 20, 30]]).evaluate();
// ➔ ["List", 11, 22, 33]
Broadcasting also applies to lazy collections like Range:
ce.assign('sq', ce.parse('x \\mapsto x^2'));
ce.expr(['sq', ['Range', 1, 4]]).evaluate();
// ➔ ["List", 1, 4, 9, 16]
Broadcasting is decided by the evaluated argument, not its written form: an argument that only evaluates to a collection — e.g. a call to a list-returning function — broadcasts too, whatever the body of the applied function is:
ce.assign('lst', ce.parse('n \\mapsto \\lbrack n, -n \\rbrack'));
ce.assign('k', ce.parse('x \\mapsto \\begin{cases} 1 & x > 0 \\\\ -1 & x \\leq 0 \\end{cases}'));
ce.expr(['k', ['lst', 3]]).evaluate(); // lst(3) ➔ [3, -3]
// ➔ ["List", 1, -1]
The static type of an application mirrors this: applied to a visible
collection, the result types list<R>; applied to an argument that may be
a collection (e.g. an unknown-return call), it types broadcastable<R>. For
a function literal, R is the type of the body with each parameter typed as
the element the call maps over, so a parameter that flows into the result
unchanged does not read unknown:
ce.parse('f(x, y, z) := (-y, z, x)').evaluate();
ce.box('f').type;
// ➔ (unknown, unknown, unknown) -> tuple<number, unknown, unknown>
ce.parse('f([1, 2], [3, 4], [5, 6])').type;
// ➔ list<tuple<integer, integer, integer>>
For a function with a declared signature, R is the declared return type
(g: (number) -> integer applied to [1, 2] types vector<integer^2>, a
list of integers). See the
broadcastable type in the Types guide.
Tuple Arguments
A function whose parameter is declared with a scalar type (a number type
or boolean) also maps over a tuple argument, as Sin((a, b)) is
(sin(a), sin(b)). The result is a tuple, typed tuple<…> with one component
type per result. A component that does not match the declared type is an error
in its own cell.
ce.assign('g', ce.parse('(u: \\mathrm{real}) \\mapsto 2u'));
ce.parse('g((a, b))').evaluate();
// ➔ ["Tuple", ["Multiply", 2, "a"], ["Multiply", 2, "b"]]
ce.parse('g((a, b))').type;
// ➔ tuple<real, real>
// A list of tuples maps over the list, then over each tuple
ce.expr(['g', ['List', ['Tuple', 1, 2], ['Tuple', 3, 4]]]).evaluate();
// ➔ ["List", ["Tuple", 2, 4], ["Tuple", 6, 8]]
A parameter with no declared type, or declared string, binds the tuple
whole: Apply(u ↦ 7, (a, b)) is 7. The compiled code gives the same tuple:
an array on the javascript target, and a vecN on the GLSL and WGSL targets.
Opting Out
To author a function that intentionally consumes a list (rather than
broadcasting elementwise over it), declare the parameter type
explicitly as list<…> or another collection type. The function will
then receive the collection as a single argument.
ce.declare('len', { signature: '(list<number>) -> integer' });
ce.assign('len', ce.parse('L \\mapsto \\operatorname{length}(L)'));
ce.expr(['len', ['List', 1, 2, 3]]).evaluate();
// ➔ 3 (no broadcasting; len receives the list as a whole)
For lambdas defined with \mapsto without an explicit signature, the
inferred parameter type is unknown, which is treated as scalar — so
they broadcast by default, unless the body shows that the parameter is a
collection.
One such case needs no annotation. When the body hands the parameter to an
operator that consumes a whole collection — Mean, Median, Variance,
StandardDeviation, Mode, Quartiles, Max, Min, Sum, Product,
GCD, LCM, Flatten, ListFrom, SetFrom or TupleFrom — a list
argument binds whole. The parameter is followed through element-wise
operators (mean(v^2), sin(v)) and through a local of the body assigned
from it.
ce.assign('h', ce.parse('v \\mapsto \\operatorname{mean}(v^2)'));
ce.expr(['h', ['List', 1, 2, 3]]).evaluate();
// ➔ 14/3 (the mean of [1, 4, 9]; not [1, 4, 9])
ce.assign('s', ce.box(['Function', ['Sum', ['Power', 'v', 2]], 'v']));
ce.expr(['s', ['List', 1, 2, 3]]).evaluate();
// ➔ 14
ce.assign('mx', ce.parse('v \\mapsto \\max(v)'));
ce.expr(['mx', ['List', 1, 2, 3]]).evaluate();
// ➔ 3
// A body with no such operator still broadcasts
ce.assign('q', ce.parse('v \\mapsto v^2 + 1'));
ce.expr(['q', ['List', 1, 2, 3]]).evaluate();
// ➔ ["List", 2, 5, 10]
A scalar argument gives the scalar value (h(2) is 4). Compiled JavaScript
follows the same rule.
A function that passes a collection to itself at a parameter's position
also receives that parameter whole: with
f(n, s) = s if n == 0 else f(n - 1, [1, 2]), f(1, [0]) is [1, 2], and
the parameter s is inferred as list<any>. See "When Inference Triggers"
in the Types guide.
Closures
A closure is a function literal that captures one or more free variables
from its defining lexical scope. The capture happens by simply referencing the
variables in the function body. For example, the following function literal
captures the variable a from its lexical scope:
["Function", ["Add", "a", "_"], "_"]
The function captures the binding of a where it is defined, not the
value of a at that time. When the function is applied, it reads the value
that this binding holds then, so an assignment to the same a made after the
definition is visible to the function:
["Block",
["Declare", "a", "integer"],
["Assign", "a", 10],
["Assign", "f", ["Function", ["Add", "a", "_"], "_"]],
["Assign", "a", 20],
["f", 1]
]
// ➔ 21
A variable of the same name declared in another scope is a different binding, which the function does not read:
["Block"
["Assign", "f",
["Block",
["Declare", "a", "integer"],
["Assign", "a", 10],
["Function", ["Add", "a", "_"], "_"]
]
]
["Declare", "a", "integer"],
["Assign", "a", 100]
["f", 1]
]
// ➔ 1 + 10
The inner a, assigned 10, is the binding the function captured. The
outer a, assigned 100, is a different variable with the same name, so the
function does not read it. The same rule applies to a function called in
the body: a call reads the function that its name denotes where the body is
written, and a same-named function defined in another scope does not redirect
it (see the Evaluation guide).
Operating on Functions
Function(body)
Function(body, arg-1, arg-2, ...)
Create a function literal which can be used as an anonymous function.
The arg-n arguments are symbols which are the bound variables (parameters) of the
function literal.
The Function operator has the lazy flag set to true, which means
that neither the body nor the parameters are evaluated until the function is
applied to some arguments.
The body is an expression that is evaluated when the function is applied to some arguments.
To apply some arguments to a function expression, use ["Apply"].
["Function", ["Multiply", "x", 2], "x"]
["Function", ["Add", ["Multiply", "x", 2], "y"], "x", "y"]
Typed parameters. A parameter may be annotated with a type by writing it
as a ["Typed", _symbol_, _type_] expression instead of a bare symbol. The
type is a string such as {"str": "integer"} (or a type-name symbol, which
is normalized to a string).
["Function", ["Add", "x", 1], ["Typed", "x", "'integer'"]]
The literal then types as a named signature — the example above has type
(x: integer) -> integer — and, in strict mode, its arguments are checked
against the declared types when the function is applied (see Typed).
Return type. To ascribe a return type, wrap the body in a Typed
expression:
["Function", ["Typed", ["Add", "x", 1], "'integer'"], ["Typed", "x", "'integer'"]]
The annotation is authoritative: it sets the result type of the signature
directly. When the function is applied, a concrete returned value (a number, a
string, a character, a boolean, or a list or tuple of these) is checked
against the declared result type: with g declared (integer) -> integer
and the body n / 2, g(4) is 2 and g(5) is an incompatible-type
error. A symbolic value, Missing, Nothing, NaN and an error value pass
unchanged, and a real float passes under integer or rational, so .N()
of such a function gives the float. Compiled code does not make this check.
Canonicalization moves the ascription inside the body Block,
wrapping its last statement, so the canonical form of the example above is:
["Function",
["Block", ["Typed", ["Add", "x", 1], "'integer'"]],
["Typed", "x", "'integer'"]]
Generic literals. A whole-signature annotation carrying a where clause
makes the literal generic (see Generic Signatures).
It may be written as a signature string in place of the parameter list:
["Function", ["Add", "x", "x"], "'(x: T) -> T where T: number'"]
or as a full-signature Typed marker on the body — which is also the
canonical form the sugar above lowers to:
["Function",
["Block", ["Typed", ["Add", "x", "x"], "'(x: T) -> T where T: number'"]],
"x"]
The signature is the single source of truth for the parameter types: a
parameter whose type mentions a type variable is erased to a bare symbol,
while a ground one (["Typed", "n", "'integer'"]) keeps its annotation and is
checked as usual. Each application solves the variables against the arguments,
so ["Apply", _literal_, 21] above evaluates to 42 and the bound number
is enforced. A where clause on an individual parameter annotation is not
accepted — type variables are introduced by a whole-signature clause only.
Type annotations round-trip through MathJSON but are dropped when serializing to LaTeX (there is no LaTeX notation for them).
Assign(symbol, fn)
Assign the function literal fn to the symbol symbol.
Assign is not a pure function,
as it changes the state of the Compute Engine.
The fn is a function literal, which can be created using the ["Function"]
expression or the shorthand function literal.
["Assign", "double", ["Function", ["Multiply", "x", 2], "x"]]
Apply(function, expr-1, ...expr-n)
Apply a list of arguments to a function. The function is a function literal, a symbol whose value is a function literal, or an expression whose value is a function.
The expr-n arguments are the arguments of the function literal.
["Apply", ["Multiply", "_", "_"], 3]
// ➔ 9
["Apply", ["Function", ["Multiply", "x", "x"], "x"], 3]
// ➔ 9
An expression that is not a function literal is read as a
shorthand function literal: ["Apply", ["Add", "x", 1], 2] is 3. An expression whose type is unknown is not:
it is kept as a function value, and the application evaluates once the value
is known. With g undefined, ["Apply", ["g", "f"], "x"] stays as written,
and the same expression written with a compound head, [["g", "f"], "x"],
is the same application. Once g is a function of two arguments, g(f) is
that function applied to its first argument, and ["Apply", ["g", "f"], "x"]
is g(f, x): a function applied to fewer arguments than it declares is a
function of the remaining ones.
["Apply", ["Function", ["Add", "x", "y"], "x", "y"], 1]
// ➔ ["Function", ["Add", "_1", 1], "_1"]
["Apply", ["Apply", ["Function", ["Add", "x", "y"], "x", "y"], 1], 2]
// ➔ 3
A function literal applied to more arguments than it declares evaluates to an
unexpected-argument error.
With LaTeX, the \lhd and \rhd commands can be used to apply a function to a single
argument on the left or right respectively.
["Apply", "f", ["Apply", "g", "x"]]
The right-pointing form is a pipeline operator: \rhd (also
\triangleright, \vartriangleright, ⊳, or the plain-text shortcut |>)
feeds the expression on its left to the function on its right, and stages
chain left to right.
The corresponding expression is Pipe(value, function). For example, Epsil
x |> f constructs Pipe(x, f) and evaluates by applying f to x:
["Pipe", "x", "f"]
// Evaluates as ["f", "x"]
The right-hand side of a pipe must be applicable. A number, string, or boolean
literal can never be applied, so piping into one is an incompatible-type
error: 5 \rhd 3 errors rather than staying inert. A symbol without a
definition is not an error — the pipe stays symbolic and evaluates once the
symbol is defined. Note that Pipe is stricter here than Apply, which
treats a non-function operand as a constant: ["Apply", 3, 5] evaluates
to 3 (this shorthand is relied on by operators such as Map).
A \square topic marker in the right-hand side names the position the piped
value fills, so a stage can be a multi-argument call:
["Solve", ["Equal", ["Power", "x", 2], 4], "x"]
(In Epsil, the topic marker is spelled _: xs |> Take(_, 10).)
The topic marker may be left out when it would fill the first slot: a call stage that is missing required arguments receives the piped value as an implicit first argument. A call that is already complete is never rewritten — the implicit argument only fills a hole.
["Pipe", ["Range", 1, 10], ["Take", 3]]
// Evaluates as ["Take", ["Range", 1, 10], 3] ➔ [1, 2, 3]
A unary function literal on the right-hand side of a pipe maps over a
collection topic instead of being applied to the collection as a whole (an
implicit Map); in Epsil, xs |> x |-> x^2 and xs |> _^2 are both
Map(x ↦ x², xs).
["Pipe", ["List", 1, 2, 3], ["Function", ["Power", "x", 2], "x"]]
// Evaluates as ["Map", …, ["List", 1, 2, 3]] ➔ [1, 4, 9]
A stage that is a function symbol always receives the whole value —
xs \rhd \operatorname{Sum} sums the collection, it does not map — as do a
string topic (a scalar in a pipeline) and a lambda whose annotated parameter
accepts the whole collection.
A bare function command such as \ln, \lb or \sqrt acts as a function
reference (12 \rhd \ln is ln(12)), and the prefix form (\rhd f, with no
left-hand side) denotes the anonymous unary function that applies f to its
argument.
Operators whose unknown/variable argument is inferable — Solve, D,
Series, and the polynomial operators such as Apart — may be piped into
without naming the variable: x^2 = 4 \rhd \operatorname{Solve} solves for
x.
EvaluateAt(function, point)
EvaluateAt(function, lower, upper)
The value of function at point, or, with two points, the difference
function(upper) − function(lower), the bracket of the fundamental theorem of
calculus.
["EvaluateAt", ["Function", ["Power", "x", 2], "x"], 3]
// ➔ 9
["EvaluateAt", ["Function", ["Divide", ["Power", "x", 3], 3], "x"], 0, 1]
// ➔ 1/3
The function is applied as Apply applies it: a function literal, a symbol
whose value is a function, or an expression, which is read as a shorthand
function literal of its free variables. A point that is a Tuple with one
coordinate per parameter gives the value at that point of several variables:
\left.\left(xy\right)\right|_{x=1, y=2} is
["EvaluateAt", ["Function", ["Multiply", "x", "y"], "x", "y"], ["Tuple", 1, 2]],
which is 2. A point is never a partial application: with fewer points than
the function requires, the expression stays as written.
When the bracket does not name the variable and the expression has several
free variables, the point or the bounds are for x: \left.kx\right|_{2}^{5}
is 3k. Write \left.\left(kt\right)\right|_{t=2}^{5} to name another
variable.
The variable of a derivative, of a series or of an indefinite integral is
free in its result, so subs() of that variable produces an EvaluateAt:
["D", ["Power", "x", 2], "x"] with x replaced by 3 is the derivative
at 3, ["EvaluateAt", ["Function", ["D", ["Power", "x", 2], "x"], "x"], 3],
which evaluates to 6. A value with no closed form stays an unevaluated
EvaluateAt. The value of a series at a point keeps its BigO remainder,
which is NaN under .N(): wrap the series in Normal to drop it.