Named Functions
Named Functions
Grid infers an independent type for every source-authored named function.
Ordinary SUM has a numeric result contract, including when a helper accepts
array inputs. Errors still propagate through the evaluator; inference does not
turn an invalid result into a number.
Definitions still use the compact existing syntax:
identity(value) = value
twice(value) = value * 2
amount(row) = row.amount
A1 = identity(42)
A2 = identity("forty-two")
A3 = twice(21)
A4 = amount({ amount: 12, currency: "USD" })The inferred signatures are:
identity : (a) -> a
twice : (number) -> number
amount : ({ amount: a | r }) -> aa is a type variable. Each call receives a fresh instance, so identity
can be used with unrelated types without one call specializing the others.
r is an open record row. It says that amount requires an amount field
but accepts any additional fields. A concrete object without that field is a
compile error.
Callable Parameters And Local Helpers
A parameter can itself be a callable. Grid infers its argument and result types from its use:
apply_to(operation, value) = operation(value)
compose_at(outer, inner, value) = outer(inner(value))
doubled = apply_to(value => value * 2, 4)
composed = compose_at(value => value + 1, value => value * 2, 4)doubled is 8 and composed is 9. The same rule applies to parameters of
LAMBDA and arrow lambdas. Bind a helper locally with DO or LET:
local_result = DO
increment = value => value + 1
increment(5)
END
excel_form = LET(increment, LAMBDA(value, value + 1), increment(5))Both results are 6. Local bindings are sequential and lexical, so later
bindings may use earlier ones. Each use of a polymorphic local helper receives
fresh type variables, as with a named definition.
Names are case-insensitive. Ordinary data bindings leave built-in calls available, so existing helpers such as this still work:
parse_year(value) = VALUE(LEFT(value, 4))
parsed_year = parse_year("2026-09-25")Here value supplies text and VALUE(...) invokes the built-in conversion.
A local binding already inferred as a function takes precedence over a built-in:
custom_value = DO
value = amount => amount + 100
VALUE(2)
ENDcustom_value is 102. When a parameter matches a built-in and is not yet
inferred as callable, a call using that name selects the built-in and retains
a data-only constraint on the parameter. Passing a function later cannot
reinterpret that call. Prefer descriptive callback names such as operation
or transform_item that do not collide with built-ins. This choice is made
during compilation; evaluation does not dispatch on the parameter's value.
Callables stay within expression scope. They may be passed to helpers and
consumed by calls; they cannot become ordinary cell values, STATE, collection
elements, or choice fields. A wire-bound lambda remains a callable declaration,
not a mutable closure value in the store.
Invocation requires a statically inferred callable type. An unknown or
any-typed value cannot be invoked by treating it as a function. In particular,
LAMBDA(f, f(f)) would require an infinite type and is rejected at compile time
with a named diagnostic. This rule also rejects fixed-point combinators and
recursive callable types. A wire-bound lambda that refers to itself or to
another wire lambda in a cycle (Loop = LAMBDA(n, Loop(n - 1))) is rejected
with GRID_DEF_RECURSION, the same diagnostic as a named-definition cycle.
Passing a callable into a wire that then calls it back (Apply = LAMBDA(f, v, f(v)) with an argument that itself calls Apply) is not recursion and is
admitted. The runtime depth cap remains an additional check; it is not a way
to opt into recursion.
Collection Protocols
Higher-order collection helpers infer requirements on the collection constructor rather than hard-coding a single container:
same_shape(values) = MAP(values, value => value)The inferred signature is:
Mappable f => (f<a>) -> f<a>The protocol names are Sized, Iterable, Sequence, Keyed, Mappable,
Foldable, and Scannable. They use the same native collection capability
matrix as runtime execution. For example, List, Deque, Map values, Ranked Set,
Trie, Interval Index, Multimap values, Stream entry field values, Record, Option, and
Result satisfy Mappable; Set does not. Stream maps through its streaming
traversal even though it is not an ordinary Iterable collection. Passing a
known non-mappable collection to same_shape is therefore rejected during
compilation rather than becoming a runtime #VALUE!.
IndexAccess is a separate operation-specific obligation for numeric positional
INDEX/OPTIONAL_INDEX: a spreadsheet array or a kind already admitted by
native Keyed. It does not add a native capability or admit every Sequence.
The obligation remains visible in source signatures and portable callable
metadata. Result shape can stay any when indexing may return a scalar or a
row/slice. A generic unknown-key at(values,key) = INDEX(values,key) still
requires Keyed; its array overload is not inferred from a numeric call site.
An exactly two-cell string-first row retains the legacy key/value-pair
representation, so it must not be assumed to be an ordinary scalar vector.
The f<a> notation is schematic rather than a claim that every collection has
one type argument. Instantiating f with Map<key, value> retains the key
contract and maps only the value: Map<k, a> -> Map<k, b>. Result mapping can
visit either active variant, so a generic one-callback wrapper requires its
success and error payload contracts to agree; use MATCH when the two branches
need different transforms.
MAP preserves its complete collection shape, FILTER preserves its complete
input type, REDUCE returns the accumulator type, and SCAN preserves the
sequence shape while changing its element type to the accumulator type.
One Name, One Definition
A canonical function name owns one definition and one generalized type scheme. Grid does not form overload sets from several same-name definitions, even when their arities or inferred types differ. Use one polymorphic definition when the behavior is shared, or give distinct behavior distinct names.
Duplicate local definitions report GRID_DEF_DUPLICATE. Two flat
EXPOSING imports that contribute the same function name report
GRID_DEF_IMPORT_CONFLICT; use USE "…" AS alias to keep independent
libraries namespaced. This keeps call resolution lexical, deterministic, and
independent of import order.
Forward Calls And Partial Application
Definitions are collected before calls are resolved, so a call can precede its
definition. Local definitions shadow same-named builtins throughout the module.
Self-recursion and cycles between definitions are rejected with
GRID_DEF_RECURSION; forward visibility is not recursive execution.
Use _ for a missing argument when passing a named function as a callback:
tax(rate, amount) = rate * amount
taxes = MAP({ 10, 20, 30 }, tax(0.05, _))This creates an immutable callable with the rate bound and one missing amount. Use it in an immediate call or a supported higher-order callback position. These callables are not ordinary cell data: storing one in a cell, exporting it across a model boundary, or treating it as a capability is unsupported. The same placeholder form works for supported builtins. Direct calls still require the declared arity.
Effects
The signature also carries an inferred effect row. Connector-backed calls use
the canonical label job_queue:<FUNCTION>:
fetch(url) = HTTP_JSON(url)fetch : (a) -> any ! {job_queue:HTTP_JSON}An empty effect row means pure. Contracts also identify required authority,
determinism, termination, incremental eligibility, and the execution route.
Connector-effect callbacks (job_queue:*) are not admitted by pure MAP,
FILTER, REDUCE, or SCAN; use the supported
bounded traversal contract when
appropriate. An inline lambda callback may read volatile sources such as
RAND() or NOW(); that makes the enclosing formula non-deterministic and
ineligible for incremental evaluation. A named or partially applied function
passed as a callback must be effect-free in this version. Capability-backed
network reads have a stricter one-stage tail-call contract; they cannot be
moved into a traversal callback. See
external functions.
Compatibility And Diagnostics
Named functions remain non-recursive. One typed callable definition is shared
by its direct call sites rather than copying a LET expansion at every call.
Inference happens once at the definition and the scheme is instantiated at
each call:
GRID_DEF_TYPEmeans the definition body contains incompatible known types.GRID_DEF_CALL_TYPEmeans concrete call arguments do not satisfy the inferred signature or collection protocol.GRID_DEF_IMPORT_CONFLICTmeans two visible definitions would claim the same canonical function name.GRID_CALLABLE_TYPErejects invalid lexical callable use, including self-application, unknown callable invocation, and escaping callable values.- Arity errors and unsupported callable-storage positions are compile errors.
Grid is gradual at external boundaries. An untyped cell reference or a
specialized builtin without a compiler signature contributes any; it does
not cause a speculative scalar rejection. Invoking such an unknown value as a
callable is rejected because no finite callable contract has been established.
Literal values, authored type contracts,
object fields, operators, control flow, lambdas, and the core higher-order
collection operations remain statically checked.
Scalar mismatches that loose coercion can represent remain source-compatible:
they emit the type diagnostic as a warning. strict promotes them to compile
errors. Structural violations—missing required record fields, incompatible
collection constructors, and absent collection protocols—are always errors
because scalar coercion cannot repair them.
FUNCTION(name) includes the inferred type, effects, callable identity,
semantic fingerprint, captures, and structured contract. Required network
selectors and logical secret purposes may appear; grants and credentials never
do. WHY(target) explains the selected callable route and dependency boundary;
for capability calls it can also show admission/refusal and cache identity.