77 Functions
Functions name pure operations that may be called from expressions. They may be used from derived attributes, constraints, function bodies, fold definitions, program expressions, and other ordinary expression contexts. Operations that perform effects are procedures, specified in the Updates part.
Function declarations are written at component declaration scope, alongside types, constants, and folds. This chapter covers function signatures, expression-bodied function definitions, external function declarations, and calls. Block-bodied functions are specified in the Updates part because they use the statement language.
Expression-bodied functions
An expression-bodied function is declared with the func keyword, a result type, a
function name, zero or more ordinary parameters, and an expression body after =:
func Boolean overlaps(TimeSlot a, TimeSlot b) =
a.day = b.day
∧ a.startMinute < b.endMinute
∧ b.startMinute < a.endMinute;
Plain parameters are read-only and non-owning. They are in scope only inside the function body, are expression variables rather than attributes, and cannot be updated or consumed. A parameter name hides a same-named value or variable while the body is checked. The body is checked in the context of the declared result type. The encoded body type must be assignable to the declared result type.
An expression body is read-only by construction. It cannot update locations, introduce mutable state, perform I/O, or depend on statement evaluation order. A function with a block body can still be pure if its effect analysis proves that it touches only local variables and calls only pure functions; see Block-Bodied Functions.
Owned parameters
A function parameter prefixed with ^ is an owned variable:
func String appendGreeting(^String x)
{
x += "hi";
return x;
}
An owned parameter may be read, updated, or consumed. Updating it is local
state and does not make the function impure. The caller may initialize it from
an ordinary value expression or explicitly move from a caller-owned variable.
Functions cannot declare & parameters because modifying a caller's
variable is an effect; procedures provide exclusive reference parameters. See
Parameter Access and Ownership.
External functions
An external function is declared inside an extern block. The block has a
linkage string that identifies the implementation domain. Function declarations
inside the block have read-only parameters, omit the func
keyword, and have no body:
extern "C++:std"
{
@domain("Sine")
Float64 sin(Float64 x);
Float64 atan2(Float64 y, Float64 x);
}
Metadata may be attached to individual external functions. Metadata is not attached to the
extern block itself. An external declaration contributes a typed function symbol to
the schema and records the linkage string; it does not provide a Predica expression body.
An external function declaration is a promise that the linked implementation behaves as a
pure function. External operations that perform effects are not functions.
Function calls
A function call names a visible function and supplies exactly one ordinary argument for each ordinary parameter:
sin(angle)
Geometry.distance(a, b)
identity<Integer>(value)
Name resolution may find an overload set containing functions from ordinary lexical lookup, selected imports, qualification, or argument-dependent lookup. The Name Resolution and Function Overload Resolution chapters specify how the set is assembled and how a unique function is selected. A function name is call-only: a function cannot be used as a first-class value merely by writing its name.
A call-shaped expression whose name resolves to a concrete nominal type, such as
Point(1, 2), is the positional shorthand for a nominal constructor, not an
ordinary function call. If the same spelling could resolve both as a function
call and as a nominal constructor shorthand, the expression is ambiguous and must
be rejected statically.
Every call is checked against the resolved function signature. The argument count must match exactly. Each argument is checked in the context of the corresponding parameter type, and the argument type must be assignable to that parameter type. Ordinary function parameters are single-valued: an argument must have static cardinality one after any cardinality-flow refinements from the surrounding guards. The call expression has the declared result type of the resolved function and cardinality one.
A plain argument initializes a read-only parameter directly and initializes an
owned parameter by value. An argument of the form ^s may be used
only for an owned parameter; its argument expression must denote an initialised
caller-owned location. The value is moved from that location into the
parameter, leaving the caller's location uninitialised. The
^ marker is call syntax rather than a general unary expression.
Receiver-call syntax
func Boolean overlaps(TimeSlot a, TimeSlot b) =
a.day = b.day
∧ a.startMinute < b.endMinute
∧ b.startMinute < a.endMinute;
overlaps(slot, other)
slot.overlaps(other)
The expression slot.overlaps(other) is a receiver-call form of the ordinary
function call overlaps(slot, other). The name after the dot resolves to a
visible identifier-named function. The receiver expression supplies the first
ordinary argument, and the arguments inside the parentheses supply the remaining
ordinary arguments.
value Square { Real side; }
value Rectangle
{
Real width;
Real height;
}
func Real area(Rectangle r) =
r.width * r.height;
embed Square s isa Rectangle =
Rectangle { width = s.side; height = s.side };
var square = Square { side = 5 };
area(square)
square.area()
Both calls resolve to the same function. The receiver call square.area() is
equivalent to area(square): the receiver square supplies the first argument.
The call is valid because the embedding makes Square a subtype of Rectangle,
so a Square value is assignable to a parameter declared as Rectangle.
A receiver call is path-expression syntax, not a method declaration. Functions are still declared at component declaration scope; they are not stored inside nominal types, inherited by nominal types, overridden by subtypes, or dispatched dynamically. Resolution is static and type-directed. If no visible function matches the receiver and ordinary arguments, the receiver call is a static name-resolution error.
value TimeSlot
{
Boolean overlaps;
}
slot.overlaps
slot.overlaps(other)
The parentheses distinguish the two cases. The expression slot.overlaps
navigates to the overlaps attribute. The expression slot.overlaps(other)
is a receiver call and resolves only as a function call. The existence of an
attribute with the same name does not make the receiver call ambiguous, and the
existence of a function with that name does not make slot.overlaps denote a
function value.
A dotted call may have the token shape of either a qualified function call or a
receiver call. The expression Geometry.distance(a, b) is a qualified function
call when Geometry resolves as a component or namespace qualifier and
Geometry.distance resolves to a visible function. The expression s.area() is
a receiver call when s resolves as an expression and area resolves to a
visible function whose first parameter can accept the receiver. If both
interpretations are possible, the call is ambiguous and must be rejected
statically; the program must use a different alias or variable name to make the
intended meaning clear.
When the receiver expression reaches many values, the receiver-call segment is
applied once for each receiver, just like an attribute path segment. For a
single-valued receiver, R.f(A, B) is equivalent to f(R, A, B). For a
multi-valued receiver, the result is the extension produced by applying the
function to each receiver member. Each receiver member is supplied as an
exactly-one first argument to the function. The arguments written inside the
parentheses are ordinary function arguments and must themselves have cardinality
one after any cardinality-flow refinements.
Names and overload sets
Functions with the same source name form an overload set. The set may contain ordinary and generic functions and may be assembled from more than one namespace by imports and argument-dependent lookup. Two declarations in the same namespace are duplicates only when they declare the same name, generic parameter structure, and ordinary parameter types; differing result types or ownership markers alone do not distinguish overloads. Function Overload Resolution selects among distinct overloads using their instantiated ordinary parameter types.
Symbolic and comparison functions
A function name may be either an identifier or a symbolic function name. A symbolic function name is an expression operator token that names a pure function directly in the function-name position. Symbolic function names are needed for user-defined comparison operations and for type-directed pure functions whose natural spelling is an operator.
| Kind | Symbolic function names |
|---|---|
| Equality | =, ≠ |
| Ordering | <, >, ≤, ≥ |
| Subtype and membership predicates | ⊂, ⊃, ⊆, ⊇, ⊄, ⊅, ∈, ∉ |
| Arithmetic | +, -, *, /, ^ |
| Set operations | ∩, ∪, ∖ |
| Path, tuple, and alternative operations | ⊓, ⊔, ⨝, | |
| Implication and equivalence | ⇒, →, ←, ↔ |
| Logic | ∧, ∨, ¬ |
The set-difference name uses the canonical set-minus character ∖. As described
in the lexical grammar, ordinary keyboard backslash is accepted as an input alias.
value Rectangle
{
Real width;
Real height;
}
value Rhombus
{
Real side;
Real angle;
}
func Boolean =(Rectangle r, Rhombus h) =
r.width = r.height ∧ r.width = h.side ∧ h.angle = 90;
A symbolic function declaration contributes a type-directed implementation of the corresponding
operation for values of the declared parameter types. Several types may define the same symbolic
operation, such as =, provided overload resolution for a particular expression is unambiguous.
Equality on values of the same concrete type is normally predefined; explicit equality declarations
are most useful when they define equality between distinct types whose values may overlap.
In the example above, rectangles and rhombi overlap only at squares, so neither type is simply a
subtype of the other.
Once equality is available for every required pair of member types, union and intersection type
expressions over those members can be used coherently.
Expression operators keep their ordinary precedence and parsing rules. A declaration of a
symbolic function such as ≤ supplies the comparison operation selected by type
checking; it does not make ≤ an ordinary first-class value and it does not change how
comparison expressions are parsed.
For ordering, ≤ is the primitive comparison associated with an ordered type. The
ordering chapter specifies the order axioms, implicit ordering, explicit order
declarations, and the derived comparison operators <, >, and
≥.
Generic functions
Functions may have generic parameters and generic where constraints:
func T identity<Type T>(T value) = value;
func Integer addN<Integer N = 2>(Integer value) = value + N;
extern "C++:math"
{
T clamp<Type T, Integer N = 3>(T value)
where T isa Real, N ∈ 1..4;
}
The where clause after a function signature is a generic constraint clause. It is
valid only when the function has a generic parameter list. It is not an ordinary precondition on
the run-time arguments.
A generic function application creates or reuses a concrete function instantiation with canonical generic arguments. Expression-bodied native generic instantiations have a Predica expression body produced by substituting the generic arguments into the source body. Generic block bodies use the same generic substitution model before their statement body is checked. External generic instantiations preserve the external linkage and have no body.
Generic arguments may be written explicitly, as in identity<Integer>(1). They may
also be inferred when inference is unambiguous from ordinary argument types and trailing defaults.
When a required generic parameter cannot be inferred, the call must write explicit generic
arguments. The generic chapter specifies the shared generic parameter and canonicalisation rules.
Rendering and hidden instantiations
Concrete function declarations created only to represent generic instantiations are schema-internal
implementation details. Rendered schema output preserves the generic declaration shell and renders
calls using the canonical generic application, such as identity<Integer>(1), rather
than emitting hidden concrete function definitions.