Skip to content

Language reference

Functions and closures

Compose named functions, callbacks and captured environments.

Use fn to declare a function, annotate its parameters and optionally write its result type after ->. Function bodies end with a result expression. Export a declaration when another module must import it.

Functions as values

kalandra
fn double(value: int) -> int:
  value * 2

fn apply(value: int, transform: fn(int) -> int) -> int:
  transform(value)

fn main() -> int:
  callback = double
  apply(21, callback)

This returns 42. A function type records parameter types, result type and any expected-error channel. A function-valued binding can be passed, returned or invoked with ordinary call syntax.

Capture a value

kalandra
fn makeAdder(offset: int) -> fn(int) -> int:
  fn(value: int): value + offset

fn main() -> int:
  addTen = makeAdder(10)
  addTen(32)

This returns 42. The returned closure retains its captured environment after makeAdder returns. Scalar captures are value snapshots. Anonymous bodies may also use an indented suite with local bindings. Rebinding a captured scalar changes only the current call's private copy. It does not update the captured environment, a later call, or the caller's binding. Array captures are read-only aliases to retained backing storage.

Array access contracts

Ordinary array parameters and array captures provide read access. Declare a mut parameter when the helper needs to write its caller's numeric or Bool array. Changing a parameter's local name is different from writing its array elements. A function type records the same access requirement, even if the function is stored in another binding or record field.

kalandra
fn writableIdentity(mut values: [int]) -> [int]:
  values

fn main() -> int:
  values = [1, 2, 3]
  forward: fn(mut [int]) -> [int] = writableIdentity
  length(forward(values))

This example returns 3. Its function type preserves the mut requirement even though writableIdentity itself only returns the array. A fresh local array or an independent copy can satisfy a mut contract. An ordinary parameter or captured array cannot acquire write permission merely through a local alias, a function return or an indirect call. Use copy when a function needs independent writable storage.

Tasks receive parent-provided arrays read-only. A spawned call cannot request mut access to those arrays; it can create and edit its own private storage. While any unfinished task may read an array, mutating calls through every alias are rejected. Await every relevant reader before writing. Awaiting only one of several readers, awaiting in only one conditional branch, or awaiting inside a loop that may run zero times does not establish unconditional settlement. When two parameters might alias, the checker preserves the same protection.

Callbacks and execution

map calls its callback sequentially. parallelMap schedules callbacks on CPU workers. Both accept checked function values and supported closures. A closure passed to a worker must capture values that can be shared with that worker. GPU tasks also accept CPU-created callbacks when the compiler can identify every possible target and all captured values are device-compatible. See the GPU numerical guide for capture types, ownership rules and examples.

Recursive calls

kalandra
fn factorial(value: int) -> int:
  if value <= 1:
    1
  else:
    value * factorial(value - 1)

fn main() -> int:
  factorial(5)

This returns 120. Functions can call themselves and each other on the CPU, including in CPU tasks. Recursive calls consume execution resources; very deep recursion can exhaust them. Do not depend on tail-call elimination for correctness.