LAMBDA and higher-order functions

LAMBDA, LET, MAP/REDUCE/BYROW/BYCOL/SCAN/MAKEARRAY, and GROUPBY/PIVOTBY

The formula engine ships Excel's full LAMBDA family: LAMBDA as both an IIFE and a value, LET for named bindings, the six standard higher-order functions (MAP, REDUCE, BYROW, BYCOL, SCAN, MAKEARRAY), and GROUPBY / PIVOTBY built on the same infrastructure. Dependency tracking through lambda bodies is automatic.

LAMBDA

Two shapes — both work:

IIFE (call inline):

=LAMBDA(x, x*x)(5)         → 25
=LAMBDA(a, b, a+b)(2, 3)   → 5

As a value (pass to a HOF or store on a named range):

=MAP(A1:A3, LAMBDA(v, v*2))

Internally a standalone LAMBDA(...) rewrites at compile time to __RNC_LAMBDA__("params", "body source"), which constructs a runtime lambda value {__isLambda, params, body}. HOFs unwrap that value and re-evaluate the body once per element with bindings substituted in.

A recursion guard caps depth at 256 to keep runaway lambdas from blowing the JS stack.

LET

Bind names to values inside a formula. Source-level expansion: each binding's value is inlined into the body before parse.

=LET(n, 5, n*2)                  → 10
=LET(a, 3, b, 4, a+b)            → 7
=LET(name, "World", "Hello "&name) → "Hello World"

Nested LET works:

A LET-bound LAMBDA can be called like a function — the engine substitutes Function-shaped occurrences of binding names with the lambda source, then the IIFE inliner collapses it:

Closures via LET capture

LET expands BEFORE LAMBDA, so any LET binding referenced inside a LAMBDA body is baked into the body source. This gives you closures for free:

Higher-order functions

MAP

Apply a lambda to each element of an array, returns a same-shape result:

Multiple input arrays of the same shape:

REDUCE

Left fold:

SCAN

Like REDUCE but returns intermediate accumulator values, same shape as input:

BYROW / BYCOL

Apply a lambda to each row (or column) as a 1D array:

MAKEARRAY

Build an N×M array by invoking a lambda with (r, c) per cell (1-indexed per Excel):

GROUPBY

Aggregate rows by row-field tuple, calling a lambda once per group on the group's value vector:

Args:

Arg
Purpose

row_fields

column or 2D — each row defines a group key tuple

values

column or 2D — values to aggregate per group

function

LAMBDA receiving the group's value vector (1D array) and returning a scalar

[field_headers]

0 (default) / 1 — echo the leading header row in output

[total_depth]

0 (default) / 1 grand-total at top / -1 grand-total at bottom

[sort_order]

1 (default, asc) / -1 (desc) by the first row-field column

[filter_array]

optional boolean column same length as row_fields; rows where FALSE are excluded

The aggregation lambda receives the group's value vector as an Excel array literal ({a, b, c}) substituted into the body source — so any aggregator that accepts an array works (SUM, AVERAGE, COUNT, MAX, MIN, custom LAMBDAs, etc.).

PIVOTBY

Two-axis cousin of GROUPBY — adds a column axis on top of the row grouping:

Output is a 2D spilled array — a header row of distinct column-tuples, then one row per distinct row-tuple prefixed by row labels.

Dependency tracking through lambda bodies

When a lambda body references a cell, the dep parser walks the body string via a fresh inner parser instance and registers each ref as a precedent of the outer formula. Recalc propagates correctly:

Named LAMBDAs (workbook scope)

Define a named range whose value is a LAMBDA and call it like a function:

See Named ranges for the full story on workbook + sheet scoping.

What's not yet supported

Recursive LAMBDA=LET(fact, LAMBDA(n, IF(n<=1, 1, n*fact(n-1))), fact(5)) doesn't work yet. Body-reparse at runtime loses the LET binding for fact. A runtime registry that survives per-call re-parse is the fix; deferred.

See docs/getting-started/excel-google-sheet-compatibility.md → "Phase 7" for the full status.

Last updated