Metaprogramming
Method calls in a reified lambda, and like / ilike
The six method calls that may appear in a reified lambda, and the exact matching rules of string.like and string.ilike.
since 0.1.0-alpha.1linuxwindowswasm
Description
A method call is reified as expr::Call(name, receiver, arguments), but only for these calls. The list is
fixed, and growing it is a language change, not something a library can do.
| receiver type | method | arguments |
|---|---|---|
string |
like |
1 |
string |
ilike |
1 |
string |
startsWith |
1 |
string |
endsWith |
1 |
string |
contains |
1 |
Array<T> for any T |
contains |
1 |
The receiver is identified by its checked type. A call that type-checks but is not in the table, such as
u.name.toUpper(), is rejected with cannot reify non-whitelisted call 'toUpper', and the error also
lists the allowed calls.
like and ilike are ordinary string methods that any program can call. In a reified lambda they have
the same meaning, and a consumer that translates the tree to SQL maps them to LIKE patterns, so this
page defines them exactly.
Allowed calls become Call nodes
class User {
string name;
new User(string n) { name = n; }
}
string dump(expr::Node n) {
match (n) {
expr::Bin => {
expr::Bin b = n;
return "Bin(${b.op}, ${dump(b.l)}, ${dump(b.r)})";
}
expr::Call => {
expr::Call c = n;
string arg = "";
for (expr::Node a in c.args) {
arg = dump(a);
}
return "Call(${c.name}, ${dump(c.recv)}, ${arg})";
}
expr::Field => {
string p = n.path.joinToString(".");
return "Field(${p})";
}
expr::Bind => { return "Bind(${n.slot})"; }
expr::Lit => { return "Lit"; }
else => { return "?"; }
}
}
void show(expr::Expr<(User) => bool> e) {
console.writeln(dump(e.tree));
}
show((u) => u.name.like("A%"));
show((u) => u.name.ilike("a%") && u.name.startsWith("A"));
show((u) => u.name.endsWith("a") || u.name.contains("d"));
Call(like, Field(name), Lit)
Bin(&&, Call(ilike, Field(name), Lit), Call(startsWith, Field(name), Lit))
Bin(||, Call(endsWith, Field(name), Lit), Call(contains, Field(name), Lit))
Rules
Matching rules of like and ilike. The pattern is matched against the whole string, as in SQL
LIKE, and matching is done on bytes.
%matches any run of bytes, including none._matches exactly one byte. A UTF-8 character that takes two bytes needs two_.\makes the next byte of the pattern literal, so\%,\_and\\match a percent sign, an underscore and a backslash. A\that is the last byte of the pattern matches a literal backslash at the end of the text.- The empty pattern matches only the empty string. A run of
%at the end is the same as one. likecompares bytes exactly, so it is case sensitive.iliketreats the ASCII lettersAtoZandatozas equal in the text and in the pattern, including the byte that follows a\. Everything outside ASCII is compared exactly:ilikenever folds a non-ASCII letter.
Examples
like and ilike
void row(string text, string pattern) {
console.writeln("'${text}' ~ '${pattern}': like=${text.like(pattern)} ilike=${text.ilike(pattern)}");
}
row("hello", "h%");
row("hello", "%llo");
row("hello", "h_llo");
row("hello", "h_lo");
row("Hello", "h%O");
row("", "%");
row("", "");
row("hello", "");
row("50%", "50\\%");
row("505", "50\\%");
row("a_b", "a\\_b");
row("axb", "a\\_b");
row("a\\", "a\\");
row("hello", "hello%%%");
'hello' ~ 'h%': like=true ilike=true
'hello' ~ '%llo': like=true ilike=true
'hello' ~ 'h_llo': like=true ilike=true
'hello' ~ 'h_lo': like=false ilike=false
'Hello' ~ 'h%O': like=false ilike=true
'' ~ '%': like=true ilike=true
'' ~ '': like=true ilike=true
'hello' ~ '': like=false ilike=false
'50%' ~ '50\%': like=true ilike=true
'505' ~ '50\%': like=false ilike=false
'a_b' ~ 'a\_b': like=true ilike=true
'axb' ~ 'a\_b': like=false ilike=false
'a\' ~ 'a\': like=true ilike=true
'hello' ~ 'hello%%%': like=true ilike=true
Matching is by byte, which matters for text outside ASCII:
Bytes, not characters
console.writeln("héllo".like("h_llo"));
console.writeln("héllo".like("h__llo"));
console.writeln("HÉllo".ilike("hÉllo"));
console.writeln("HÉllo".ilike("héllo"));
false
true
true
false
A generic method can accept a reified predicate that uses an allowed call, so a typed query builder works:
A generic query builder
class User {
string name;
new User(string n) { name = n; }
}
class Query<E> {
Array<string> filters = [];
Query<E> where(expr::Expr<(E) => bool> p) {
expr::Node t = p.tree;
match (t) {
expr::Call => {
expr::Call c = t;
filters = filters.add(c.name);
}
else => { filters = filters.add("other"); }
}
return this;
}
}
Query<User> q = Query();
q.where((u) => u.name.like("A%")).where((u) => u.name.endsWith("z"));
console.writeln(q.filters.joinToString(", "));
like, endsWith
class User { string name; }
// error: cannot reify non-whitelisted call 'toUpper'
// note: reifiable calls: string.like/1, string.ilike/1, string.startsWith/1, string.endsWith/1, string.contains/1, Array.contains/1
expr::Expr<(User) => bool> e = (u) => u.name.toUpper() == "A";
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.
- like — Match the whole string against an SQL
LIKEpattern. - ilike — Match the whole string against an SQL
LIKEpattern, ignoring ASCII letter case.