---
title: "Lambdas and reducers"
description: "Arrow functions, function values, and the loops that replace the reducer methods of kScript (legacy) (map, filter, reduce, forEach, find, some, every). kScript…"
order: 16
section: "core-concepts"
---

<!-- source: docs/indicators/core-concepts/lambdas-and-reducers.md; generated by packages/cli/scripts/gen-indicator-docs.ts, do not edit -->

# Lambdas and reducers

Arrow functions, function values, and the loops that replace the reducer
methods of kScript (legacy) (`map`, `filter`, `reduce`, `forEach`, `find`,
`some`, `every`). kScript v3 added JavaScript-style lambdas that close
over their surroundings and a reducer family with iteration guards. An
Indicator has arrow functions too, with one rule that changes how you use
them: a function cannot capture a local variable, so the "closure" is
module-level state and a reducer is a plain loop over a buffer.

## Lambda syntax

An arrow function has typed parameters and a typed return, in either the
expression or the block form. It is a value: store it in a variable typed
`(x: f64) => f64`, pass it to a function, call it.

```text
const double = (x: f64): f64 => x * 2.0;
const label = (side: f64): string => {
  return side > 0.0 ? "bid" : "ask";
};

let pick: (x: f64) => bool = above;   // a named function is a value too
const y = double(21.0);               // 42
```

Both forms need every type written out: an untyped parameter or a missing
return type is a parse error (`Type expected.`).

## The one rule: no captured locals

A kScript lambda closed over the surrounding scope: `var threshold = high
* 0.99; prices.filter((p) => p > threshold)`. In an Indicator that exact
shape, an arrow function inside `state()` reading a `const` of `state()`,
is refused at compile time:

```text
ERROR AS100: Not implemented: Closures
```

The compiler cannot build a function that carries a copy of another
function's locals. What a function **can** read is module-level state,
which is where an Indicator keeps everything that matters anyway. So the
pattern is: put the threshold in a module-level `let`, write the predicate
as a named function (or an arrow at module scope), and pass it.

```typescript
import { input, line, lower, ohlcv, output, param } from "./sdk/declare";
import { in_close } from "./gen/inputs";
import { emitRow, out_above_count, out_near_high_count } from "./gen/outputs";
import { p_bars } from "./gen/params";

param("bars", 20, { min: 1, max: 200, description: "Bars in the window" });
input("close", ohlcv.close);
output("above_count", line, lower, { color: "#16a34a", description: "Bars in the window closing above the window mean" });
output("near_high_count", line, lower, { color: "#f59e0b", description: "Bars in the window within 1% of the window high" });

const MAX_BARS = 200;
const closes = new StaticArray<f64>(MAX_BARS);
let n: i32 = 20;
let cursor: i32 = 0;
let count: i32 = 0;
let mean: f64 = NaN; // module-level: the "closure" every predicate reads
let nearHighLevel: f64 = NaN;

// Predicates are named functions over module-level state, never over a local.
function aboveMean(x: f64): bool {
  return x > mean;
}

function nearHigh(x: f64): bool {
  return x >= nearHighLevel;
}

// A reducer is a loop that takes the predicate as a value.
function countWhere(xs: StaticArray<f64>, len: i32, pred: (x: f64) => bool): i32 {
  let hits = 0;
  for (let i = 0; i < len; i++) if (pred(xs[i])) hits += 1;
  return hits;
}

function windowMax(xs: StaticArray<f64>, len: i32): f64 {
  let m = -Infinity;
  for (let i = 0; i < len; i++) if (xs[i] > m) m = xs[i];
  return m;
}

function windowMean(xs: StaticArray<f64>, len: i32): f64 {
  let s = 0.0;
  for (let i = 0; i < len; i++) s += xs[i];
  return s / f64(len);
}

export function init(): void {
  n = i32(p_bars());
}

export function state(): i32 {
  closes[cursor] = in_close();
  cursor = (cursor + 1) % n;
  if (count < n) count += 1;
  if (count < n) return 0;
  mean = windowMean(closes, n);
  nearHighLevel = windowMax(closes, n) * 0.99;
  return 1;
}

export function finalize(): void {
  out_above_count(f64(countWhere(closes, n, aboveMean)));
  out_near_high_count(f64(countWhere(closes, n, nearHigh)));
  emitRow();
}

export function reset(): void {
  cursor = 0;
  count = 0;
  mean = NaN;
  nearHighLevel = NaN;
}
```

`countWhere` is the reducer; `aboveMean` and `nearHigh` are the lambdas,
reading `mean` and `nearHighLevel` from module scope instead of capturing
them. The same function value can be passed to any loop that takes a
`(x: f64) => bool`.

## The reducer methods

`Array<f64>` has the familiar methods, and each callback is a non-capturing
function with typed parameters (trailing parameters may be omitted):

| Method | Callback | Returns | Allocates? |
| --- | --- | --- | --- |
| `map<U>(fn)` | `(value: f64, index?: i32) => U` | a new array | yes |
| `filter(fn)` | `(value: f64, index?: i32) => bool` | a new array | yes |
| `reduce<U>(fn, initial)` | `(acc: U, value: f64, index?: i32) => U` | the final accumulator | no |
| `forEach(fn)` | `(value: f64, index?: i32) => void` | nothing | no |
| `findIndex(fn)` | `(value: f64, index?: i32) => bool` | the first matching index, or `-1` | no |
| `some(fn)` | `(value: f64, index?: i32) => bool` | `true` if any match | no |
| `every(fn)` | `(value: f64, index?: i32) => bool` | `true` if all match | no |

There is no `find`: use `findIndex` and read the element. `map` and
`filter` return a **new** array every call, and the module never frees
memory, so they belong in `init()` (building a lookup table once) and not
in `state()`. On the per-bar path, write the loop over a `StaticArray` you
allocated once; the window example above is the template, and
`collections.md` has every numeric reducer as a method.

```text
// init(): fine, once
const doubled = periods.map<f64>((p: f64): f64 => p * 2.0);

// state(): a loop over a preallocated buffer, no allocation
let sum = 0.0;
for (let i = 0; i < n; i++) sum += window[i];
```

## The microstructure idiom

Reducers were how kScript turned raw order-flow rows into indicator
values: `vp.cells.map((c) => c[2] - c[3]).reduce((s, x) => s + x, 0)`.
The Indicator version reads the same cells (one `[low, high, buy, sell]`
row per price bucket) into a preallocated buffer and loops over them: the
net delta of the bar, and the price of its largest-volume bucket (the
point of control).

```typescript
import { input, line, lower, ohlcv, output, overlay, volume_profile } from "./sdk/declare";
import { in_profile_capacity, in_profile_cells, in_profile_read } from "./gen/inputs";
import { emitRow, out_delta, out_poc } from "./gen/outputs";

input("close", ohlcv.close);
input("profile", volume_profile.cells, { max_cells: 512 });
output("delta", line, lower, { color: "#2563eb", description: "Net per-bar delta: buy minus sell volume across every price bucket" });
output("poc", line, overlay, { color: "#f59e0b", description: "Price of the bucket that traded the most volume" });

const cells = new StaticArray<f64>(in_profile_capacity); // max_cells tuples of 4 f64, allocated once
let delta: f64 = NaN;
let poc: f64 = NaN;

export function init(): void {}

export function state(): i32 {
  const n = in_profile_cells();
  if (n <= 0 || in_profile_read(i32(changetype<usize>(cells))) < 0) return 0; // no block this bar
  let net = 0.0;
  let best = -1.0;
  let bestPrice = NaN;
  for (let i = 0; i + 3 < n; i += 4) {
    const buy = cells[i + 2];
    const sell = cells[i + 3];
    net += buy - sell; // the map + reduce, as one pass
    if (buy + sell > best) {
      best = buy + sell;
      bestPrice = (cells[i] + cells[i + 1]) / 2.0; // the bucket's midpoint
    }
  }
  delta = net;
  poc = bestPrice;
  return 1;
}

export function finalize(): void {
  out_delta(delta);
  out_poc(poc);
  emitRow();
}

export function reset(): void {
  delta = NaN;
  poc = NaN;
}
```

Every windowed class in the TA kit accepts any number, so `rsi.update(delta)`
is a delta-RSI in one line. The celled input, its accessors, and the
`max_cells` contract are in `data-sources.md`.

## Loops still exist, and you bound them

`for`, `while`, and `do` are the iteration tools (`for...of` is not
implemented: `Not implemented: Iterators`, so index loops it is). There is
no per-loop ceiling inside the module; size every loop by a param with a
declared `max`. A loop that runs away is stopped by the host's execution
timeout and the evaluation is refused: it cannot hang the chart, and it
cannot produce a value either.
