hypereffect

Expressions

Attribute values can be computed from signals with CEL expressions. The renderer re-evaluates them whenever the signals they read change.

CEL

Expressions are written in the Common Expression Language (CEL), a small language designed for evaluating untrusted expressions quickly and safely. It looks like the expression part of C, Java or JavaScript:

s.count + 1
s.user.name + ' (' + string(s.user.unread) + ')'
s.saved ? 'success' : 'failure'
size(s.items) == 0

CEL has no side effects and is not Turing-complete: evaluating an expression cannot change signals or send messages, and its cost is bounded by the size of the expression and its input. It has independent implementations in Go, Java, C++, Rust, JavaScript, Python and others.

Syntax

An expression is a JSON object with a single cel member holding the expression as a string. It takes the place of a literal value:

element {"id":"static","type":"text","text":"Hello"}
element {"id":"computed","type":"text","text":{"cel":"'Hello, ' + s.user.name"}}

Any other value is literal, including objects with other members or with a non-string cel.

"text":     {"cel":"'Count: ' + string(s.count)"}
"visible":  {"cel":"s.count > 0"}
"disabled": {"cel":"s._busy || s.login.user == ''"}
"variant":  {"cel":"s.saved ? 'success' : 'failure'"}

The s variable

Expressions have a single variable, s, which holds the current signals. With the signals {"count":3,"user":{"name":"Ada"},"_open":true}, the expressions s.count, s.user.name and s._open evaluate to 3, "Ada" and true. Keys that are not CEL identifiers can be read with an index, as in s['first-name'].

Where expressions are allowed

Any attribute may be an expression except the structural ones, which must be literal: id, type, parent, before, bind and the handlers (onPress, onChange, onSubmit, onDismiss). The component reference marks each attribute that accepts an expression.

Inside a handler, the leaf values of a patch may be expressions. They are evaluated against the current signals before the patch is applied, which allows a toggle:

"onPress": {"patch":{"_expanded":{"cel":"!s._expanded"}}}

Functions

Every renderer provides the CEL standard definitions and the strings extension, listed below. Functions written x.f() are called on a value; the others are called on their own or through their namespace.

Standard

Defined in the CEL language definition.

Operator or function Description
! && || Logical not, and, or. && and || tolerate an error on one side if the other side decides the result.
c ? a : b Conditional.
== != < <= > >= Comparison. Numbers of different types compare by value.
+ - * / % Arithmetic on numbers of the same type. + also concatenates strings and lists. - also negates.
a in b Whether a list contains a value, or a map has a key.
a[i] List index or map key.
size(x), x.size() Length of a string (in code points), list or map.
str.contains(t) Whether str contains t.
str.startsWith(t), str.endsWith(t) Prefix and suffix tests.
str.matches(re), matches(str, re) Whether str contains a match of the RE2 regular expression.
int(x), uint(x), double(x) Number conversions, from numbers or strings.
string(x) Converts a number, bool, timestamp or duration to a string.
bool(x) Parses "true" or "false".
bytes(x) Converts a string to bytes.
dyn(x) Marks a value as dynamically typed.
type(x) The type of a value, comparable with int, string, list, map and so on.
timestamp(str), duration(str) Parse an RFC 3339 timestamp, or a duration such as "1h30m". Timestamps and durations support +, - and comparison.
t.getFullYear(), getMonth(), getDate(), getDayOfMonth(), getDayOfWeek(), getDayOfYear(), getHours(), getMinutes(), getSeconds(), getMilliseconds() Timestamp components, in UTC or in the time zone given as an argument. getHours() to getMilliseconds() also apply to durations.

Macros:

Macro Description
has(x.f) Whether the map x has the key f: has(s.user).
l.all(x, p) Whether p holds for every item. On a map, iterates over keys.
l.exists(x, p) Whether p holds for some item.
l.exists_one(x, p) Whether p holds for exactly one item.
l.map(x, e), l.map(x, p, e) The list of e for each item, optionally only those for which p holds.
l.filter(x, p) The items for which p holds.

Strings

Defined by the cel-go strings extension. Indexes count code points.

Function Description
str.charAt(i) The character at index i, or "" at the end of the string.
str.indexOf(t), str.indexOf(t, from) Index of the first occurrence of t, or -1.
str.lastIndexOf(t), str.lastIndexOf(t, from) Index of the last occurrence of t, or -1.
str.substring(start), str.substring(start, end) The characters from start up to end, exclusive.
str.lowerAscii(), str.upperAscii() Changes the case of ASCII letters only.
str.trim() Removes leading and trailing whitespace.
str.replace(old, new), str.replace(old, new, n) Replaces occurrences of old, all of them or the first n.
str.split(sep), str.split(sep, n) Splits into a list, into at most n parts if given.
l.join(), l.join(sep) Joins a list of strings.
str.reverse() The characters in reverse order.
str.format(args) Formats a list of values, printf style: "%s has %d items".format([s.name, size(s.items)]). Verbs: %s, %d, %f (with precision, as %.2f), %e, %b, %o, %x, %X, %%.
strings.quote(str) The string as a quoted CEL string literal.

Types

Signals are JSON, and are seen by CEL as follows:

JSON CEL
null null_type
true, false bool
number with no fraction or exponent int
any other number double
string string
array list(dyn)
object map(string, dyn)

CEL does not mix int and double in arithmetic. If a signal can hold either, convert it first: double(x) * 2.0.

The result must match the attribute's type. String and enum attributes need a string, with no implicit conversion, so numbers must go through string() or format(). Bool attributes need a bool. Number attributes accept int, uint or double.

Failure

If an expression fails to parse, fails to evaluate, or produces a value of the wrong type, the attribute takes its default. The renderer reports this as a diagnostic, never to the user.

Reading a signal that does not exist is an evaluation failure. The default of visible and open is true, so a failed expression shows the element. Send signals before the elements that read them, or test for them with has():

"text": {"cel":"has(s.user.name) ? s.user.name : 'guest'"}

The exact rules are in the specification.