Quoted expressions
A parameter or binding typed Expression<Value> gets the ordinary value plus the typed tree of what was written, its source text and the values it captured, which is what assert and a query provider read instead of running the code twice.
Diese Seite gibt es noch nicht auf Deutsch, deshalb steht sie hier auf Englisch. Zur englischen Seite
Not every node is built yet. Both back ends build a quoted expression as a value - its
tree,source,location,value()and the encoded valuescaptures()answers - but not the node of a map literal, of a range, of a field reached through the implicitself, or of a top-levelconstof a script that is neither a compile-time constant nor a number,BoolorString: a program that needs one of those is refused bytorb runwith a message that names it.torb checkaccepts them, and the rules are the language's.
A parameter or a binding typed Expression<Value> checks its argument as an ordinary Value and additionally hands
over the typed tree of what was written, its exact source text, and the values any captured variable held. assert
is the one function of the prelude area that every program already uses this way.
Example#
use assert from "std/expression"
const limit = 5
const numbers = [1, 2, 3]
assert(numbers.length() < limit)Syntax#
<parameter>: Expression<<Type>>
const <name>: Expression<<Type>> = <expression>// std/expression
native type Expression<Value> {
tree: ExpressionNode
source: String
location: SourceLocation
native fn value(): Value
native fn captures(): List<EncodedValue>
}Rules#
The call site of an
Expression<Type>parameter looks exactly like a call toTypeitself. Nothing marks the argument;assert(condition: Expression<Bool>)is called asassert(numbers.length() < limit), the same as if the parameter were a plainBool.value()answers the ordinary value, evaluated at most once, and for a quoted function type it answers the closure rather than calling it. Nothing is evaluated beforevalue()asks:nameOf(items[9])reads the name and never indexes the list.sourceis the exact text that was written, andtreeis the same thing as data, anExpressionNodebuilt from literals, parameters, captured variables, field access, calls, constructors, operators,if/else, nested closures, list literals and string interpolation. A tuple, a map literal and a range are constructions and are quotable too;?.and??have no node of their own, because they are the calls onOptionthey mean.Quoting happens after name resolution and type checking. The tree only ever contains resolved names: an implicit
_, a named closure parameter, a receiver and an implicitselfare already the explicitParameter,FieldorCallnode they resolved to, with a type on every node.A quoted closure is exactly one expression, because a quotation as a whole is one. A statement inside it -
const,var, an assignment, a loop,return, or the early-return?- is a compile error, and so ismatch.fn describePredicate(predicate: Expression<(Int) => Bool>): String { predicate.source } const minAge = 18 print describePredicate({ age => age >= minAge })Kompiliert nichtfn describePredicate(predicate: Expression<(Int) => Bool>): String { predicate.source } const minAge = 18 print describePredicate({ age => const isAdult = age >= minAge isAdult }) // error: A `const` cannot appear in a quotation: `Expression<(Int64) => Bool>` holds a single expressionKompiliert nichtfn describeCondition(condition: Expression<Bool>): String { condition.source } const value: Int? = Some(1) print describeCondition(match value { Some(_) => true None => false }) // error: `match` cannot be quoted yetA captured variable has to implement
Encode. A provider has to be able to look at what a quotation captured (a SQL driver binds it as a query parameter), so capturing anything else is a compile error at the capture, naming the parameter that required it.Kompiliert nichtuse assert from "std/expression" shared type Session { name: String } fn check(session: Session) { assert(session.name == "a") } // error: `session` is captured here and `Session` is not `Encode`The tree costs nothing when there is nothing captured, and one small allocation per evaluation when there is.
assertis in every test, which is why a quotation without captures is free and one with captures is not free but still cheap.The tree cannot be executed, only the value can. There is no
compile()step: an in-memory caller readsvalue(), a provider such as a database driver readstreeand translates what it understands, and fails its own call at its own runtime for a node it does not - the language cannot know in advance what a library can translate.
What this is not#
Expression<Value> is not reflection. TypeReference on a node is a name and its type arguments, and there is no
way from it back to the type itself; the tree can be matched, built and passed around like any other value, but it
cannot look anything up that was not already written at the call site.
use nameOf from "std/expression"
type User {
email: String
}
const user = User "ada@example.test"
print nameOf(user.email)use nameOf from "std/expression"
type User {
email: String
}
const user = User "ada@example.test"
print nameOf(user.unknown)
// error: `User` has no member `unknown`nameOf reads the name that was already resolved on the tree; it cannot read a name that does not exist, because the
argument is type checked as an ordinary expression before it is ever quoted.
A quotation is not a macro. It runs no code of its own during compilation and it cannot change what a program means; it is ordinary data available after the fact, built from a call that has already been fully checked.
Related#
- Parameter modes -
Expression<Type>next tovar,lazyand receiver closures. - Trailing closures - the closure form a quoted function type also takes.
- Cases and match - why
matchneeds a node set of its own before it can be quoted.