Zekken Documentation

Function declarations, lambda values, and call semantics.

Section 4 of 8

Functions

Declaring Functions#

Zekken declares named functions with func. Parameters are written inside pipes and must include types. Use return to return a value. If a function finishes without a return, it evaluates to void.

func add |a: int, b: int| {
  return a + b;
}

func greet |name: string| {
  @println => |"Hello, " + name|
  // no return => void
}

Calling Functions#

All calls use the same shape: callee => |arg1, arg2|. Zero-argument calls use ||.

let sum: int = add => |1, 2|;
@println => |sum|

let nums: arr = [1, 2, 3];
let first: int = nums.first => ||;
@println => |first|

Return Values And Types#

Zekken checks argument values against parameter types at call time, and checks return values against the receiving variable's declared type.

func half |n: float| {
  return n / 2.0;
}

let ok: float = half => |9.0|;
// let bad: int = half => |9.0|; // Type Error (int <- float)

Lambda Values (Anonymous Functions)#

Lambdas are values. The syntax uses fn -> followed by a normal parameter list and block body. Because the lambda is assigned to a variable, the let statement ends with ;.

let sub: fn -> |a: int, b: int| {
  return a - b;
};

let out: int = sub => |5, 3|;
@println => |out|

Passing Functions Around#

The type annotation for function values is fn. Zekken currently does not encode full function signatures in the type annotation, so you should document expected arguments in your API design and tests.

func apply_twice |f: fn, x: int| {
  let a: int = f => |x|;
  let b: int = f => |a|;
  return b;
}

let inc: fn -> |n: int| { return n + 1; };
let y: int = apply_twice => |inc, 10|;
@println => |y| // 12

Built-Ins And Native Functions#

Built-ins are native functions. The only difference is that built-ins use the @ prefix (for example, @println and @input). Native functions exposed by libraries are stored as members on library objects and are called via member access (for example, math.sqrt) without the @ prefix. If you import a library function directly (for example, use { hex_encode } from encoding;), you call it like a normal function (hex_encode => |...|) with no @ prefix. Both forms use the same call operator: => |...|.

use math;
use { hex_encode } from encoding;

@println => |"Type something:"|
let name: string = @input => |"Name: "|;
@println => |"hi " + name|

let encoded: string = hex_encode => |"Hello World!"|; // 48656C6C6F20576F726C6421
@println => |encoded|

let root: float = math.sqrt => |9.0|; // 3.0
@println => |root|

Nested Calls#

You can nest calls inside argument lists. Because calls also use pipes, nested calls look like "extra" pipe characters. If you find this hard to read, assign the inner call to a variable first.

// Nested
@println => |add => |1, 2||

// Often clearer
let s: int = add => |1, 2|;
@println => |s|

Notes On Terminators#

  • let/const declarations end with ;.
  • Named function declarations (func name ... { ... }) do not use a trailing semicolon.
  • Many other statements (like @println calls) do not use semicolons.

Optional Explicit Return Types#

You can optionally annotate a function's return type using ->. If present, Zekken enforces that the returned value matches the annotation at runtime.

func add |a: int, b: int| -> int { // Type error (int <- float)
  return "This is a string";
}

func half |n: float| {
  return n / 2.0;
}

@println => |add => |1, 2||
@println => |half => |9.0||