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); // 42Both 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 insidestate()reading aconstofstate()`, is refused at compile time:
ERROR AS100: Not implemented: ClosuresThe 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):
| 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.
// 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.