The template language
Expressions
Values, operators and precedence, calls, pipes and lambdas.
Anywhere a value goes can compute it with an expression: attribute values, {…} in strings, if conditions, for … in, and the right-hand sides of declarations.
Expressions are pure. They read names and return values, and never change anything. An expression that reads state is evaluated again when the state changes.
Values
There are six kinds of value:
| Kind | Written | Notes |
|---|---|---|
| null | null | no value |
| bool | true, false | |
| number | 3, -2.5 | 64-bit floating point |
| text | "Hello {name}" | with interpolation |
| list | [1, 2, 3] | items of any kind |
| record | { title: "A", done: false } | named fields, kept in name order |
{ title }is short for{ title: title }.- A trailing comma is allowed in lists, records and calls.
type_of(x)names a value's kind:"null","bool","number","text","list"or"record".
Values as text
Where a value is shown as text, in interpolation or an attribute:
| Value | Text |
|---|---|
null | nothing |
true, false | true, false |
| a number | the shortest form: 3, 2.5, 0.30000000000000004 |
| text | itself |
| a list | its items as text, joined with , |
| a record | [record] |
For more control, use format_number, percent, join and round (Standard functions).
Truthiness
Conditions (if, &&, ||, !, when) read a value as true or false: null, false, 0, empty text and an empty list are false, and everything else is true, including every record.
Names and fields
- A name reads the scope: a declaration, a prop, a companion's value, or a loop variable.
- A path,
post.author.name, reads a field, then its field. On a list,.lenis its length. - A field of a computed value is written after
),]or}:first(posts).title,{ a: 1 }.a.
Reading a name the scope doesn't have, or a field a record doesn't have, is an error. The exception is .field after ), ] or }, which gives null for a missing field. Check first with has(record, "field"), or read with get(record, "field"), which gives null.
Operators
| Operator | Does |
|---|---|
a + b | adds numbers; joins two lists; joins text when either side is text (the other side as text). A list on the left needs a list on the right |
a - b, a * b | arithmetic on numbers |
a / b, a % b | division and remainder; null when b is zero |
-a | negation |
a == b, a != b | equality (see below) |
a < b, <=, >, >= | order (see below) |
a && b, a || b | logic; the result is true or false, and the right side is only evaluated when needed |
!a | not |
a ?? b | a, or b when a is null |
if c then a else b | a when c is truthy, otherwise b |
x |> f(y) | the call f(x, y) |
{ r | a: 1 } | a copy of record r with field a set |
Arithmetic needs numbers: text that reads as a number ("42") counts as one, and anything else is an error, such as "a" * 2.
Comparison:
- When both sides read as numbers (numbers, or text such as
"10"), they're compared as numbers:"10" == 10is true, and"9" < "10"is true. - Otherwise both sides are compared as text, so lists compare by their text, and any two records are equal. Compare their fields instead.
nullis equal only tonull.
Precedence
From loosest to tightest. Every binary operator groups from the left.
| Level | Operators |
|---|---|
| 1 | if … then … else |
| 2 | ?? |
| 3 | || |
| 4 | && |
| 5 | == != < <= > >= |
| 6 | |> |
| 7 | + - |
| 8 | * / % |
| 9 | ! and - (prefix) |
| 10 | calls, fields |
So:
a ?? b + 1isa ?? (b + 1).xs |> len > 2compares the length: a pipe binds tighter than a comparison, as in Elixir.!a == bis rejected, since it's easy to misread: writea != b, or(!a) == b.a < b < ccomparesa < b, a boolean, withc. Writea < b && b < c.- An
ifextends as far right as it can, so put one inside other operators in parentheses:1 + (if big then 10 else 0).
Calls and pipes
A function is called with parentheses, f(x, y), never f x: attributes are separated by spaces, so juxtaposition would be ambiguous.
A pipe passes the value on its left as a call's first argument, so a chain reads left to right:
let shown = posts |> filter(p -> p.published) |> sort_by(p -> p.date) |> reverse |> take(5)
for p in shown { p "{p.title}" }A pipe into a function with no other arguments needs no parentheses: name |> trim |> lower.
The functions are the standard functions and any the scope adds with Scope::function, which win over a standard function of the same name. Calls to standard functions are checked when the template is parsed: the number of arguments, and where lambdas go.
Lambdas
A lambda is a function written inline, only as a function's argument:
let done = todos |> filter(t -> t.done)
let labels = todos |> map((t, i) -> text(i + 1) + ". " + t.title)
p "{done.len} done: {join(labels)}"t -> exprtakes one parameter, and(a, b) -> exprtakes two.- The lambdas of
map,filter,find,find_index,any,allandcountmay take a second parameter: the item's index, from 0. - A lambda reads the names around it, and its parameters hide them.
Records and lists, updated
Values are never changed in place. A step that "changes" a list or record builds a new one:
| To | Write |
|---|---|
| change a record's field | { t | done: !t.done } |
| add to a list | todos + [item] |
change item i | todos |> update_at(i, t -> { t | done: true }) |
replace item i | todos |> set_at(i, value) |
insert before item i | todos |> insert_at(i, value) |
remove item i | todos |> remove_at(i) |
| remove by a condition | todos |> filter(t -> t.id != id) |
Updating null with { r | a: 1 } gives a new record { a: 1 }.
List comprehensions
[expression | generators and conditions] builds a list, read left to right:
let open = [t.title | t <- todos, !t.done]
let pairs = [[x, y] | x <- xs, y <- ys, x != y]
ul { for title in open { li "{title}" } }name <- listgoes through a list, withnameas each item for everything after it. Later generators nest inside earlier ones, sopairstakes everyyfor eachx.- A condition keeps only the items for which it holds.
- A list that's
null(not loaded yet) gives nothing to go through, so the result is[].
It's a shorter way to write filter then map: [t.title | t <- todos, !t.done] is todos |> filter(t -> !t.done) |> map(t -> t.title).
To add one item, put it in brackets: todos + [item]. Without them, todos + item is an error: + only adds a list to a list.