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…

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.

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 aconstofstate()`, is refused at compile time:
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.

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):

MethodCallbackReturnsAllocates?
map<U>(fn)(value: f64, index?: i32) => Ua new arrayyes
filter(fn)(value: f64, index?: i32) => boola new arrayyes
reduce<U>(fn, initial)(acc: U, value: f64, index?: i32) => Uthe final accumulatorno
forEach(fn)(value: f64, index?: i32) => voidnothingno
findIndex(fn)(value: f64, index?: i32) => boolthe first matching index, or -1no
some(fn)(value: f64, index?: i32) => booltrue if any matchno
every(fn)(value: f64, index?: i32) => booltrue if all matchno

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.

// 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).

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.