Synsemadocsv0.6.xENES

Language

Tasks & lambdas

A task is a function: parameters, a body, and give to return a value. give leaves the task at once, even from inside a loop. Without give, a task returns whatever its last statement produced (nothing after a print) — write give whenever the result matters.

tasks.syn
-- Doc example: tasks, default args, named args, strict arity, recursion, lambdas.
intent: "doc example: tasks and lambdas"

task add(a, b)
    give a + b

task greet(name, greeting = "Hello")
    give greeting + ", " + name + "!"

task factorial(n)
    when n <= 1
        give 1
    otherwise
        give n * factorial(n - 1)

let calls be 0
task next_id()
    set calls to calls + 1
    give calls

task stamp(id = next_id())
    give id

task first_big(xs)
    each x in xs
        when x > 10
            give x
    give nothing

task no_give()
    3 + 4

task only_prints()
    print("side effect")

task twice(a, b = a * 2)
    give b

task with_total(order)
    set order["total"] to order["qty"] * order["price"]
    give order

task make_adder(n)
    give (x) => x + n

-- The message of the error `f` raises, or nothing when it does not raise.
task error_of(f)
    try
        f()
    recover err
        give err
    give nothing

print(greet("Ada") + "  factorial(5) = " + text(factorial(5)))    -- run shows the result

test "call, default arg, named arg"
    assert_eq(add(2, 3), 5)
    assert_eq(greet("Ada"), "Hello, Ada!")
    assert_eq(greet("Ada", "Hi"), "Hi, Ada!")
    assert_eq(greet("Ada", greeting = "Hey"), "Hey, Ada!")
    assert_eq(greet(greeting = "Hey", name = "Ada"), "Hey, Ada!")

test "a default is evaluated at each call"
    assert_eq(stamp() + 1, stamp())
    assert_eq(stamp(100), 100)
    assert_error(() => twice(3))
    assert_eq(twice(3, 6), 6)

test "arity is strict: missing, extra and unknown arguments are errors"
    assert(contains(error_of(() => add(1)), "task 'add' is missing argument 'b'"))
    assert(contains(error_of(() => add(1, 2, 3)), "task 'add' takes 2 arguments, got 3"))
    assert(contains(error_of(() => greet("Ada", tone = "x")), "unknown parameter 'tone'"))
    assert(contains(error_of(() => ((x) => x)(1, 2)), "this lambda takes 1 argument, got 2"))

test "give returns at once, also from inside a loop"
    assert_eq(first_big([1, 20, 30]), 20)
    assert_eq(first_big([1, 2]), nothing)
    assert_eq(no_give(), 7)
    assert_eq(only_prints(), nothing)

test "a task gets a copy of a list or map: give it back to keep the change"
    let order be {"qty": 2, "price": 5}
    let done be with_total(order)
    assert_eq(order, {"qty": 2, "price": 5})
    assert_eq(done["total"], 10)
    set order to with_total(order)
    assert_eq(order["total"], 10)

test "recursion"
    assert_eq(factorial(5), 120)

test "lambdas are values, passed to higher-order tasks"
    let triple be (n) => n * 3
    assert_eq(triple(4), 12)
    assert_eq(apply(triple, [1, 2, 3]), [3, 6, 9])
    assert_eq(where([{"active": true}, {"active": false}], (x) => x.active), [{"active": true}])

test "lambdas close over names; a task can give a lambda back"
    let k be 10
    let add_k be (x) => x + k
    assert_eq(add_k(1), 11)
    let add5 be make_adder(5)
    assert_eq(add5(1), 6)

test "a callback that a builtin calls declares only what it uses"
    assert_eq(apply(() => 0, [1, 2]), [0, 0])
    assert_eq(reduce([1, 2, 3], (acc, x) => acc + x, 0), 6)

-- v0.6.46: types on parameters, checked only where arguments arrive as data (call, call_tool).
task estado(id: text, items: bool = false)
    give {"id": id, "items": items}

task page(limit, offset)
    give [limit, offset]

test "typed parameters are checked by call, not by a normal call"
    assert_eq(call(estado, {"id": "a1"}), {"id": "a1", "items": false})
    assert(contains(error_of(() => call(estado, {"id": 5})), "'id' must be text, got number"))
    assert(contains(error_of(() => call(estado, {})), "missing 'id' (text)"))
    assert_eq(estado(5), {"id": 5, "items": false})

test "a misspelled named argument names the closest one and lists them all"
    assert(contains(error_of(() => page(limt = 1, offset = 2)), "unknown parameter 'limt' — did you mean 'limit'?; the parameters are: limit, offset"))

Default & named arguments§

task greet(name, greeting = "Hello")
    give greeting + ", " + name + "!"

greet("Ada")                               -- uses the default
greet("Ada", "Hi")                         -- positional
greet("Ada", greeting = "Hey")             -- named
greet(greeting = "Hey", name = "Ada")      -- named, any order

The default value (= expr) is evaluated at each call (it cannot refer to the other parameters). Note = here is the argument form — general assignment is let/set, and equality is ==. Builtins take their arguments by position, except the named forms they document (sort(xs, desc = true), int(s, base = 16)…).

Arity is strict§

A call must match the task: a missing argument without a default, an extra one, or an unknown name is an error — never a silent nothing or a dropped value:

A callback that a builtin or the host calls (apply, where, reduce, route handlers, cron) is the exception: it declares only the parameters it uses (apply(() => 0, xs) is fine).

Arguments are values§

A task receives a copy of a list or map (copy-on-write, so passing it costs nothing). A set inside the task changes the task's copy, not the caller's — to keep the change, give it back and set it in the caller:

task with_total(order)
    set order["total"] to order["qty"] * order["price"]
    give order

let order be {"qty": 2, "price": 5}
set order to with_total(order)

synsema check warns about a task that writes into a parameter and then neither gives it back, passes it on, nor reads it: that write would be lost.

Types on parameters§

Engine v0.6.46+. A parameter can carry a type, written after a colon, with or without a default:

task estado(id: text, items: bool = false)
    give {"id": id, "items": items}

print(call(estado, {"id": "a1"}))          -- {id: "a1", items: false}

synsema code outline --json publishes them per task, next to the unchanged params list of names:

"parameters": [
  {"name": "id", "type": "text", "required": true},
  {"name": "items", "type": "bool", "required": false, "default": false},
  {"name": "extra", "type": null, "required": false, "default": null}
],
"input_schema": {
  "type": "object",
  "properties": {"id": {"type": "string"}, "items": {"type": "boolean", "default": false}, "extra": {}},
  "required": ["id"]
}

type is null for an untyped parameter; default appears only for a literal default (null for nothing), and input_schema leaves default out for a nothing default or a computed one (= 1 + 2).

Recursion§

A task may call itself (factorial above). There is no special keyword — just give.

Lambdas§

A lambda is a value: (params) => expr. Store it, pass it, or hand it to a higher-order op:

let triple be (n) => n * 3
apply(triple, [1, 2, 3])        -- [3, 6, 9]
where(items, (x) => x.active)   -- inline predicate

A lambda sees the names around it (let add_k be (x) => x + k), and a task can give one back (task make_adder(n) … give (x) => x + n).