Synsemadocsv0.6.xENES

Lenguaje

Tasks y lambdas

Una task es una función: parámetros, un cuerpo y give para devolver un valor. give sale de la task en el acto, incluso desde dentro de un loop. Sin give, una task devuelve lo que haya producido su última sentencia (nothing después de un print) — escribí give siempre que el resultado importe.

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"))

Argumentos por defecto y con nombre§

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

El valor por defecto (= expr) se evalúa en cada llamada (no puede referirse a los otros parámetros). Ojo: = acá es la forma de argumento — la asignación general es let/set, y la igualdad es ==. Los builtins toman sus argumentos por posición, salvo las formas con nombre que documentan (sort(xs, desc = true), int(s, base = 16)…).

La aridad es estricta§

Una llamada tiene que coincidir con la task: un argumento que falta sin default, uno que sobra o un nombre desconocido es error — nunca un nothing silencioso ni un valor descartado:

Un callback que llama un builtin o el host (apply, where, reduce, handlers de ruta, cron) es la excepción: declara solo los parámetros que usa (apply(() => 0, xs) anda).

Los argumentos son valores§

Una task recibe una copia de una lista o un map (copy-on-write, así que pasarlo no cuesta nada). Un set dentro de la task cambia la copia de la task, no la del llamador — para quedarte con el cambio, devolvelo con give y hacé set en el llamador:

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 avisa de una task que escribe en un parámetro y después ni lo devuelve, ni lo pasa, ni lo lee: esa escritura se perdería.

Tipos en los parámetros§

Motor v0.6.46+. Un parámetro puede llevar un tipo, escrito después de dos puntos, con o sin 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 los publica por task, al lado de la lista params de nombres, que no cambia:

"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 es null para un parámetro sin tipo; default aparece sólo para un default literal (null para nothing), e input_schema omite default para un default nothing o uno calculado (= 1 + 2).

Recursión§

Una task puede llamarse a sí misma (factorial arriba). No hay keyword especial — solo give.

Lambdas§

Una lambda es un valor: (params) => expr. Guardala, pasala, o dásela a una operación de orden superior:

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

Una lambda ve los nombres que la rodean (let add_k be (x) => x + k), y una task puede devolver una (task make_adder(n) … give (x) => x + n).