LEVIATHAN v962456e · 962456eee1

Metaprogramming

Where a lambda literal is reified

A lambda literal becomes an expr::Expr<F> in any position whose expected type is expr::Expr<F>: a call argument, a declared variable, a return value or a field initializer.

since 0.1.0-alpha.1linuxwindowswasm

Description

A lambda literal is reified whenever the type the context expects is expr::Expr<F>. The compiler looks at the expected type, not at the lambda, so reification happens in every position that carries one:

  • a call argument, where the parameter's type is expr::Expr<F>;
  • a variable declaration (a local or a global) whose declared type is expr::Expr<F>;
  • a return statement in a function whose declared return type is expr::Expr<F>;
  • a field initializer in a class, for a field whose declared type is expr::Expr<F>.

The body must be exactly one expression: (u) => <expression>. A block body, even one that only contains return, is rejected. Every lambda parameter may be the root of a Field path, so a lambda with several parameters reifies too, with the caveat in the Rules below.

Only a lambda literal is converted. A variable that happens to hold a lambda cannot be turned into an Expr after the fact, because the tree is built from the lambda's source at compile time.

The four positions

class User {
    int age;
    new User(int a) { age = a; }
}

string show(expr::Expr<(User) => bool> e) {
    expr::Node t = e.tree;
    match (t) {
        expr::Bin => {
            expr::Bin b = t;
            return "${b.op}";
        }
        else => { return "other"; }
    }
}

class Rules {
    expr::Expr<(User) => bool> adult = (u) => u.age >= 18;
}

expr::Expr<(User) => bool> senior() {
    return (u) => u.age >= 65;
}

console.writeln("argument:    " + show((u) => u.age < 18));
expr::Expr<(User) => bool> declared = (u) => u.age == 30;
console.writeln("variable:    " + show(declared));
console.writeln("return:      " + show(senior()));
console.writeln("field:       " + show(Rules().adult));
argument:    <
variable:    ==
return:      >=
field:       >=

Rules

  • The lambda must be a literal written in the position. Passing a variable of a function type where an expr::Expr<F> is expected is a compile error (only a lambda literal can be reified to expr::Expr<F>).
  • The body must be one expression, not a block.
  • A lambda literal matches a plain function-typed parameter and an expr::Expr<_> parameter equally well. Two overloads that differ only in that way make a call with a literal ambiguous, and that is a compile error rather than a silent preference. Give the overloads different names, or assign the lambda to a typed local first to choose one.
  • A Field path has no marker for which parameter it belongs to. In (a, b) => a.age < b.age both sides are Field(["age"]). A consumer that must tell parameters apart should use single-parameter lambdas.
  • A lambda whose parameter type arrives through a generic parameter reifies like any other, so a generic where(expr::Expr<(E) => bool> p) method can be called with a literal.

Examples

Multiple parameters and a generic method

class User {
    int age;
    new User(int a) { age = a; }
}

class Query<E> {
    int count = 0;
    Query<E> where(expr::Expr<(E) => bool> p) {
        count = count + 1;
        return this;
    }
}

string side(expr::Node n) {
    match (n) {
        expr::Field => { return n.path.joinToString("."); }
        else => { return "?"; }
    }
}

expr::Expr<(User, User) => bool> younger = (a, b) => a.age < b.age;
expr::Node t = younger.tree;
match (t) {
    expr::Bin => {
        expr::Bin b = t;
        console.writeln("${side(b.l)} ${b.op} ${side(b.r)}");
    }
    else => { console.writeln("other"); }
}
console.writeln(younger.fn(User(20), User(30)));

Query<User> q = Query();
q.where((u) => u.age > 1).where((u) => u.age < 90);
console.writeln(q.count);
age < age
true
2
class User { int age; }

string pick(((User) => bool) f) => "closure";
string pick(expr::Expr<(User) => bool> f) => "expr";

// error: ambiguous lambda argument: matches both a function parameter and an expr::Expr parameter;
//        extract a typed local to select
console.writeln(pick((u) => u.age > 1));
not run — shows a compile error

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.
  • Reification errors — The compile errors a reified lambda can produce, what each means, and how to fix it.