Functions
Arguments pass by value, as in TypeScript. A function may return a number, a boolean, a
text, a unit, a record, a tuple or an array, and an instance under the rule in
Classes. Parameters take defaults (step = 1) and may be optional
(extra?: number). Local variables start afresh at every call, and a function declared
inside another sees the outer one's parameters and locals.
function split(n: number) {
return { big: n > 4, half: n >> 1 };
}
function around(n: number) {
return [n - 1, n + 1] as const;
}
function advance(n: number, extra?: number) {
return extra !== undefined ? n + extra : n + 1;
}
program(() => {
let n = 7;
const { big, half } = split(n);
const [lo, hi] = around(n);
print(`${big} ${half} ${lo}-${hi} ${advance(n)} ${advance(n, 10)}`); // true 3 6-8 8 17
});
Written out or called#
A function used once is written out where the call stands. A function used more than once is called when it can be: one copy of its body in the map, which keeps the map smaller. The meaning is the same either way, and the hint at the end of the function's line says which it got: called ×3, or inlined ×3 with the reason when you hover it. Three things keep a function written out:
- It waits. The program has to wake up inside it, which only a written-out body can do.
- A parameter reaches something only a known value can fill.
function pay(p: Player, n: number) { setResources(p, "add", n, "ore"); }needs its player when the map is built, sopay(P2, 4)is written out. The amount could be a variable; the player cannot. - It uses
rose()oronce(), which remember what they saw at each place they are written.
An array reaches a function as itself, and which array is settled when the script is
built. So a function that takes an array is one copy for each array it is passed:
total(hp) and total(shields) are two copies. The hint counts them: called ×5, 2
copies.
A function that calls itself#
A function may call itself, directly or through another: fib(n - 1) + fib(n - 2), a
flood fill over a grid, two functions that call each other. Each run has its own
parameters and locals, as in TypeScript. Three things are different from a JavaScript
engine:
- It cannot wait. No
sleep()orawaitinside it; the error says so. - It has a depth limit: 1 024 calls deep, unless the workspace's Settings view
says otherwise (16 to 65 536, kept in the map). A call past the limit is a
RangeErrorthat acatchcan take. Uncaught, it stops the program, and the game says where: stack overflow in fill, line 12. Simulate stops at the same call. - It costs what it keeps. Around each call, the function's variables are saved and restored. That is nothing for a flood fill a thousand deep, and a visible pause for tens of thousands of calls in one frame. The fewer variables, the cheaper the call.
program(() => {
const grid = [
[0, 0, 1, 0],
[0, 1, 1, 0],
[0, 0, 0, 0],
];
function fill(x: number, y: number): number {
if (x < 0 || y < 0 || y >= grid.length || x >= grid[0].length) return 0;
if (grid[y][x] != 0) return 0;
grid[y][x] = 2;
return 1 + fill(x + 1, y) + fill(x - 1, y) + fill(x, y + 1) + fill(x, y - 1);
}
let x = 0;
print(`${fill(x, 0)} cells filled`); // 9 cells filled
});
A loop over units cannot hold such a call. Collect what it finds into an array, and call the function from a loop over that.
Functions in other files#
A function declared at the top of any of the script's files runs in the game when a
program calls it with values of the program. game() marks one that only the game runs:
// shop.ts
export const award = game((p: Player, n: number) => { setResources(p, "add", n, "ore"); });
export const canAfford = game((have: number, price: number) => have >= price);
// main.ts
import { award, canAfford } from "./shop";
program(() => {
let gold: u8 = 10;
if (canAfford(gold, 5)) { gold -= 5; award(P2, 3); }
});
A game() function follows a program's rules, and what it does is shown at its own file
and line. It sees its parameters and what any file sees, not the calling program's
variables. Calling one outside a program is an error.
Functions as values#
A function can be kept in a variable, an array, a field or a Map, and returned by
another function. A call through what holds it calls whichever function is there when the
line runs. A closure keeps what it captured, as in JavaScript: counter() below gives a
function with a count of its own.
function heal(hp: number) { return hp + 10; }
function hurt(hp: number) { return hp - 10; }
function counter() {
let n = 0;
return () => { n += 1; return n; };
}
program(() => {
const effects = [heal, hurt, (hp: number) => hp * 2];
const next = counter();
let hp = 40;
let i = 0;
while (i < effects.length) { hp = effects[i](hp); i = next(); }
print(`${hp}`); // 80
});
A function handed to a method or a parameter costs nothing extra: it is copied in where it
is called. A kept one is a number in the map, and a call through it picks among the
functions that could be there, one test each. An async function or a generator is not
kept as a value: call it where it is written, or start() it.