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.
-- 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:
add(1)→task 'add' is missing argument 'b' — pass it, or give the parameter a defaultadd(1, 2, 3)→task 'add' takes 2 arguments, got 3greet("Ada", tone = "x")→unknown parameter 'tone'; the parameters are: name, greeting, y un nombre casi bien escrito nombra el más cercano (motor v0.6.46+):f(limt = 1)→unknown parameter 'limt' — did you mean 'limit'?; the parameters are: limit, offset- una lambda también:
this lambda takes 1 argument, got 2; y los builtins:append() takes at most 2 arguments, got 3
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}
- Los cinco tipos son los de
expect body:text,number,bool,list,map. Un secret cuenta comotext. - Se chequean sólo donde los argumentos llegan como datos:
call(task, map)ycall_tool(task, map)— las formas que usan una tool call del modelo y el host. Ahí el chequeo es estricto y nunca convierte:call_tool(add): 'a' must be number, got text — …, y un parámetro con tipo y sin default que el map no trae escall_tool(add): missing 'a' (number)(uno sin tipo sigue llegando comonothing).nothingse acepta sólo cuando el default del parámetro esnothing. - Una llamada normal no los chequea y no cuesta nada:
estado(5)corre. Son metadata para datos que cruzan una frontera, no un sistema de tipos estático. - Errores de carga, antes de que corra nada: un tipo desconocido (
task f(a: int)→unknown type 'int' for parameter 'a': the parameter types are text, number, bool, list or map) y un default literal de otro tipo (task f(a: number = "x")→the default of 'a: number' is a text). Las lambdas no llevan tipos:(x: number) => xno parsea. - Le describen la task a un modelo:
tool_spec(task, describe)arma la entrada del catálogo dellm_stepdesde la firma — ver Tool calling.
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).