Core Concepts

Lambdas & Reducers

Arrow-function lambdas, closures, and the reducer methods (map, filter, reduce, forEach, find, some, every) that replace most explicit loops.

Intermediate 10 min read

Introduction

kScript v3 adds JavaScript-style arrow lambdas and a family of reducer methods on arrays. Together they replace most explicit for loops with one-line transformations, and they are the recommended way to iterate: every reducer pass is automatically protected by the runtime's iteration watchdogs, and the analyzer type-checks the callback's element types.

Pine Script has no lambdas at all. A transformation that takes a Pine for loop and manual accumulator state is typically a single expression in kScript: that conciseness is measured, not aspirational (the engine's own test suite asserts a representative transformation at half the Pine line count).

Lambda syntax

var double = (x) => x * 2
var label = (p, side) => {
  var prefix = side > 0 ? "bid" : "ask"
  return format("{0} @ {1}", prefix, tostring(p))
}

var y = double(21)          // 42

Two forms:

  • Expression body: (x) => x * 2, the expression is the return value.
  • Block body: (x) => { ... return v }, with explicit return.

Lambdas are values. Store them in var variables, pass them to functions and reducers, return them from functions, and call them directly. They close over the surrounding scope:

var threshold = high * 0.99
var nearHigh = prices.filter((p) => p > threshold)

The reducer methods

All seven take a lambda and iterate a snapshot of the array (mutating the array inside the callback does not affect the active pass):

MethodCallbackReturns
map(fn)(value, index?) => newValueNew array of transformed values
filter(fn)(value, index?) => booleanNew array of passing values
reduce(fn, initial)(accumulator, value, index?) => accumulatorThe final accumulator
forEach(fn)(value, index?) => voidNothing (side effects)
find(fn)(value, index?) => booleanFirst match, or na
some(fn)(value, index?) => booleantrue if any match
every(fn)(value, index?) => booleantrue if all match
var sizes = book.map((lvl) => lvl[1])
var bigLevels = book.filter((lvl) => lvl[1] > 50)
var totalSize = sizes.reduce((sum, s) => sum + s, 0)
var hasWall = book.some((lvl) => lvl[1] > 500)

Callback arity is lenient

A callback may declare fewer or more parameters than the method supplies; parameters the method does not provide are bound to na. Direct lambda calls (not through a reducer) keep strict arity checking. This mirrors how every mainstream language treats callbacks and keeps one-parameter lambdas clean.

The microstructure idiom

Reducers are how kScript turns raw order-flow rows into indicator values. The canonical pattern, from cells to metric to plot:

timeseries fp = source("footprint", symbol=currentSymbol)

// net delta for the current bar
timeseries delta = fp.cells.map((c) => c[2] - c[3]).reduce((s, x) => s + x, 0)

// price of the largest-volume bucket
var top = fp.cells.reduce((best, c) =>
  (c[2] + c[3]) > (best[2] + best[3]) ? c : best, fp.cells[0])
plotLine(top[0], color="#f59e0b")

Every windowed indicator in the TA library also accepts microstructure-derived series, so rsi(source=delta, period=14) is a delta-RSI in one line. None of this is expressible in Pine Script: it has neither the data nor the lambdas.

Loops still exist, and they are guarded

for, for...in, for...of, and while remain available for logic that does not fit a reducer. Every loop body and every reducer pass is protected by the same watchdogs: a per-loop iteration ceiling, a per-run aggregate ceiling, and mid-bar interruption support, all reporting precise line/column errors. A runaway loop terminates loudly; it cannot hang a chart. The numbers live in Limitations.

Rule of thumb: reach for a reducer first, for...in/for...of second, and a manual indexed loop only when you genuinely need index arithmetic.