Language
Control flow & errors
-- Doc example: control flow (when / match / each / while) and errors (try / recover / raise).
intent: "doc example: control flow and errors"
enum Status
paid(amount)
shipped(days, carrier)
open
task grade(score)
when score >= 90
give "A"
otherwise when score >= 70
give "B"
otherwise
give "C"
task status_label(status)
match status
is "open"
give "active"
is "done"
give "archived"
otherwise
give "unknown"
task describe(v)
match v
is 0
give "zero"
is Status.paid(amount)
give "paid " + text(amount)
is Status.shipped(d, c) when c == "DHL"
give "express"
is [first, ...rest]
give "list of " + text(1 + length(rest)) + " starting at " + text(first)
is {name, age} when age >= 18
give "adult " + name
is {status: 200, body}
give "ok: " + body
is _
give "something else"
task shape(xs)
match xs
is []
give "empty"
is [a, b]
give "pair " + text(a + b)
is [...init, last]
give "last " + text(last) + " after " + text(length(init))
task is_target(v, target)
match v
is target
give true
otherwise
give false
task sum_list(xs)
let total be 0
each n in xs
set total to total + n
give total
task always_fails()
raise("boom")
task fails_as_statement()
raise "bad input"
task give_inside_try()
try
give "given"
recover err
give "recovered"
print("grade(85) = " + grade(85) + ", sum_list([1,2,3]) = " + text(sum_list([1, 2, 3])))
test "when / otherwise when / otherwise"
assert_eq(grade(95), "A")
assert_eq(grade(75), "B")
assert_eq(grade(50), "C")
test "match dispatches on value"
assert_eq(status_label("open"), "active")
assert_eq(status_label("done"), "archived")
assert_eq(status_label("x"), "unknown")
test "match patterns: literal, enum variant, guard, list, map, wildcard"
assert_eq(describe(0), "zero")
assert_eq(describe(Status.paid(5)), "paid 5")
assert_eq(describe(Status.shipped(2, "DHL")), "express")
assert_eq(describe(Status.shipped(2, "UPS")), "something else")
assert_eq(describe([7, 8, 9]), "list of 3 starting at 7")
assert_eq(describe({"name": "Ana", "age": 20, "city": "x"}), "adult Ana")
assert_eq(describe({"name": "Ana", "age": 10}), "something else")
assert_eq(describe({"status": 200, "body": "hi"}), "ok: hi")
assert_eq(describe("x"), "something else")
test "list patterns: [], [a, b], [...init, last]"
assert_eq(shape([]), "empty")
assert_eq(shape([1, 2]), "pair 3")
assert_eq(shape([1, 2, 3]), "last 3 after 2")
test "a bare name in an `is` arm compares with its value (it does not bind)"
assert_eq(is_target(3, 3), true)
assert_eq(is_target(4, 3), false)
test "each accumulates"
assert_eq(sum_list([1, 2, 3, 4]), 10)
test "each walks a map's keys, text's characters, bytes as integers"
let ks be []
each k in {"b": 1, "a": 2}
set ks to append(ks, k)
assert_eq(ks, ["b", "a"])
let cs be []
each c in "hé"
set cs to append(cs, c)
assert_eq(cs, ["h", "é"])
let bs be []
each b in bytes([1, 255])
set bs to append(bs, b)
assert_eq(bs, [1, 255])
test "stop leaves a loop; there is no continue — wrap the rest in when"
let seen be []
each n in [1, 2, 3, 4, 5]
when n == 4
stop
when n != 2
set seen to append(seen, n)
assert_eq(seen, [1, 3])
test "each gives a copy of each item: write back by index"
let rows be [{"p": 1}, {"p": 2}]
each i in range(length(rows))
set rows[i]["p"] to rows[i]["p"] * 10
assert_eq(rows, [{"p": 10}, {"p": 20}])
test "while repeats until the condition is false"
let n be 0
while n < 5
set n to n + 1
assert_eq(n, 5)
test "try / recover catches; raise re-raises"
let caught be ""
try
raise("boom")
recover err
set caught to err
assert(contains(caught, "boom"))
assert_error(always_fails)
assert_error(fails_as_statement)
test "give and stop pass through try untouched"
assert_eq(give_inside_try(), "given")
let n be 0
each x in [1, 2, 3]
try
when x == 2
stop
set n to n + 1
recover err
set n to 100
assert_eq(n, 1)
task message_of(f)
try
f()
recover err
give err
give nothing
task page(limit, offset = 0)
give limit + offset
test "each walks lists, not arrays: enumerate(a) gives the index and the row (engine v0.6.46+)"
let total be 0
each e in enumerate(array([5, 6]))
set total to total + e.index * 10 + e.item
assert_eq(total, 21)
assert(contains(message_of(() => enumerate(array(5))), "0-d array"))
test "messages that name the fix (engine v0.6.46+)"
assert(contains(message_of(() => log(100)), "ln(x) for the natural log, log10(x), log2(x) or log_base(x, b)"))
assert(contains(message_of(() => in_units(90, "minutes")), "in_units(duration(seconds = 90), \"minutes\")"))
assert_eq(in_units(duration(seconds = 90), "minutes"), 1.5)
assert(contains(message_of(() => page(limt = 5)), "unknown parameter 'limt' — did you mean 'limit'?; the parameters are: limit, offset"))
assert(contains(message_of(() => sort([3, 1], dsc = true)), "did you mean 'desc'?"))
assert_eq(page(limit = 5), 5)Conditionals§
when score >= 90
give "A"
otherwise when score >= 70
give "B"
otherwise
give "C"
No colon after the condition, and no if/elif/else: those words fail with the Synsema form in the message (`elif is not a Synsema statement: chain with otherwise when <condition> `). The expression form is when c then a otherwise b` — see Syntax.
match§
match dispatches on a value with rich patterns — literals, enum variants, list/map patterns, guards, and _:
match result
is "open" -- a value (compared with ==)
open_it()
is Status.shipped(days, carrier) when carrier == "DHL"
express(days) -- enum variant + binders + guard
is [first, ...rest] -- list head + tail
handle(first)
is {name, age} when age >= 18 -- map keys as binders (extra keys ignored)
greet(name)
is {status: 200, body} -- map field + binder
show_ok(body)
is _ -- wildcard
skip()
otherwise
fallback()
A bare name in an arm (is target) compares with that variable's value; it does not bind. Binders appear only inside list, map and variant patterns. List patterns also take [a, b], [] and [...init, last].
Loops§
each item in collectioniterates a list; over a map it walks the keys in insertion
order, over text its characters, over bytes integers 0–255. Each item is a copy: to change the list, write by index (set rows[i]["p"] to …) or rebuild it with apply. For an index next to each item, each e in enumerate(xs) → e.index (from 0) and e.item; since engine v0.6.46 enumerate also takes a numeric array and walks its first axis (each item a number, or a row). each itself does not walk an array (Cannot iterate over array): use enumerate(a) or to_list(a).
while conditionrepeats until the condition is false. There is no iteration cap: make sure
the condition changes.
stopleaves the loop. There is nocontinue— wrap the rest of the body inwhen
(the parser says so if you write continue).
Prefer the intentional operations (apply/where/reduce) when you're transforming a list.
Errors — try / recover / raise§
try
risky()
recover err
log "failed: " + err -- err is the message (text)
raise(err) -- RE-PROPAGATE; without it, recover swallows the error
raise(message) fails deliberately. give and stop are not errors — they pass through try/recover untouched. (fail(...) is for HTTP responses, not runtime errors.) There is one kind of error (no exception types), no except/catch and no finally: put the cleanup after the try/recover block.
The statement form raise "msg" / raise err (no parens) also works — it desugars to the same call — and a bare raise alone is a loud parse error pointing at the fix. ⚠️ On the published v0.5.1 and older, the no-parens form silently did nothing (it parsed as two inert expressions); on those binaries always write raise("msg").
Messages that name the fix§
An error says what to write instead, and synsema check warns several of them before the program runs. Since engine v0.6.46:
| You wrote | The message | Write instead |
|---|---|---|
log(x) (Python's math.log) | `Undefined variable: 'log' — in Synsema: ln(x) for the natural log, log10(x), log2(x) or log_base(x, b); log "message" (no parentheses) writes a log line (synsema check` warns it too) | ln(x), log10(x), log2(x) or log_base(x, b); log "message" is the log statement |
in_units(90, "minutes") (a number) | in_units() takes a duration, got number — make one first: in_units(duration(seconds = 90), "minutes") | build the duration first: in_units(duration(seconds = 90), "minutes") → 1.5 |
a named argument the task or builtin does not have: page(limt = 5) | unknown parameter 'limt' — did you mean 'limit'?; the parameters are: limit, offset (without a close name, only the list: unknown parameter 'zzzzzz'; the parameters are: limit, offset) | one of the names listed. Before v0.6.46 a misspelled name on a task said that the right one was missing |
The in_units hint always shows seconds and "minutes", with your number: adapt the unit you meant. An unknown named argument is found when the call runs — synsema check does not warn it. The same holds for lambdas (f(1, c = 2) on (a, b) => … lists a, b) and builtins (sort(xs, dsc = true) → did you mean 'desc'?).