Metaprogramming
Captured values and the binds array
How a reified lambda records the outside values it uses, once and in order, in the binds array, and what that guarantees.
since 0.1.0-alpha.1linuxwindowswasm
Description
A lambda often uses values from outside: a local variable, this.limit, a field of another object. The
tree cannot hold a live variable, so each such value goes into the binds array of the Expr, and the
tree refers to it with expr::Bind(slot), where slot is an index into binds.
The contract is:
binds[k]is the value of the k-th distinct captured expression. Slots are numbered in the order the captured expressions first appear in the lambda body.- Each captured value is evaluated once, when the
Expris built. The tree andbindsnever read a variable again. - Two uses of the same captured expression share one slot. Distinctness is by the chain as written: the
root name plus the member path.
loused twice is one slot, and so isthis.xused twice. - A captured chain that goes through several members, such as
this.owner.limit, is one slot holding the value of the whole chain.
The result is that a consumer can translate the tree and take the parameter values from binds, as a
database layer does when it turns the tree into a prepared statement with parameters.
Slots are shared and values are fixed at construction
class User {
int age;
string name;
new User(int a, string n) { age = a; name = n; }
}
string lit(string | int | float | bool | None v) {
match (v) {
string => { return "\"" + v + "\""; }
int => { return v.toString(); }
else => { return "None"; }
}
}
void demo() {
int floor = 18;
string tag = "x";
expr::Expr<(User) => bool> e = (u) => u.age >= floor && u.age < floor + 50 && u.name != tag;
floor = 99;
console.writeln(e.binds.length());
for (string | int | float | bool | None b in e.binds) {
console.writeln(lit(b));
}
}
demo();
2
18
"x"
Rules
- A value is snapshotted when the
Expris constructed. Changing the variable afterwards does not changebinds, and the tree still refers to the same slot. - Slot order is the order of first reference in the lambda, left to right.
- Only values of type
string,int,floatorboolcan be captured intobinds. AnArray<T>that is the receiver ofcontainsgets a slot whose entry isNone; the consumer keeps its own reference to the array. Any other captured type is a compile error. - Capturing does not run user code in the tree: a
Bindholds only its slot number. The captured expression is evaluated exactly once, as part of building thebindsarray. - The closure
fnis the ordinary lambda. The guarantees above are abouttreeandbinds.
Examples
A member chain and a shared slot
class Owner {
int limit = 40;
string name = "Ada";
}
class Account {
int age;
string name;
new Account(int a, string n) { age = a; name = n; }
}
class Checker {
Owner owner = Owner();
int minimum = 10;
void run() {
expr::Expr<(Account) => bool> e =
(a) => a.age >= this.minimum && a.age < this.owner.limit && a.age != this.minimum;
console.writeln(e.binds.length());
string | int | float | bool | None first = e.binds[0];
string | int | float | bool | None second = e.binds[1];
match (first) {
int => { console.writeln("slot 0 = ${first}"); }
else => { console.writeln("slot 0 is not an int"); }
}
match (second) {
int => { console.writeln("slot 1 = ${second}"); }
else => { console.writeln("slot 1 is not an int"); }
}
}
}
Checker().run();
2
slot 0 = 10
slot 1 = 40
An array capture
class User {
int age;
new User(int a) { age = a; }
}
Array<int> allowed = [1, 2, 3];
expr::Expr<(User) => bool> e = (u) => allowed.contains(u.age);
console.writeln(e.binds.length());
string | int | float | bool | None slot = e.binds[0];
match (slot) {
int => { console.writeln("an int"); }
else => { console.writeln("None: the array is not stored"); }
}
console.writeln(e.fn(User(2)));
console.writeln(e.fn(User(9)));
1
None: the array is not stored
true
false
See also
- Expression reification — lambdas as data — A lambda literal in a position typed expr::Expr<F> compiles to an ordinary closure plus a walkable tree of its checked body, which is what query builders translate to other languages.
- The reifiable subset — The expressions that may appear in a reified lambda and the tree node each one becomes.
- expr — The expression-reification tree: runtime, walkable descriptions of lambda bodies.