Skip to main content

Functions

A function in esque is a name bound to a single expression.

fn add(a: i32, b: i32) -> i32 = a + b

That is the whole declaration: fn name(params) -> ReturnType = body. There is no return statement — the body is the result. (return is a reserved word but has no use today.)

Multi-step bodies

Use a block to compose several let bindings before the final expression:

fn sum_of_squares(a: i32, b: i32) -> i32 = {
let sq_a = a * a;
let sq_b = b * b;
sq_a + sq_b
}

Semicolons separate steps. The final expression has no trailing semicolon — it is the return value.

Calls

fn double(x: i32) -> i32 = x * 2
fn main() -> i32 = double(21)

Order of arguments matters. Calls are eager and left-to-right.

Recursion

fn factorial(n: i32) -> i32 = {
if n <= 1 { 1 }
else { n * factorial(n - 1) }
}

fn main() -> i32 = factorial(5) # 120

esque is fine with self-recursion and mutual recursion. The compiler collects all top-level signatures in a first pass before checking any body, so order of definitions does not matter.

Lambdas

|params| body is an anonymous function. Lambdas appear only as inline arguments to higher-order primitives like tabulate, scan, and iterate_until:

fn main() -> i32 = +/(tabulate(5, |i| i*i)) # 0+1+4+9+16 = 30

Binding a lambda to a let and calling it later is not supported today — first-class function values are planned. Lambdas capture nothing (no closures over outer state). They exist to inline computation into a generic; the compiler specialises and unrolls them away.

Pipeline calls

x |> f is exactly f(x):

fn double(x: i32) -> i32 = x * 2
fn add_one(x: i32) -> i32 = x + 1
fn square(x: i32) -> i32 = x * x

fn main() -> i32 = 3 |> double |> add_one |> square
# = square(add_one(double(3))) = square(7) = 49

x |> f(y) is f(x, y):

fn add(a: i32, b: i32) -> i32 = a + b
fn main() -> i32 = 10 |> add(5) # = add(10, 5) = 15

Pipelines are just syntax. They lower to the same calls you would write by hand and produce the same machine code.

Generic over shapes

A function may declare shape parameters in [ ... ] between the name and the parameter list:

fn dot[N](x: f32[N], y: f32[N]) -> f32 = +/(x .* y)

N is a nat-valued shape parameter. Each call site infers N from the actual tensor shape and the compiler emits one specialised copy per N it sees:

fn use_a(x: f32[4], y: f32[4]) -> f32 = dot(x, y) # emits dot__4
fn use_b(x: f32[8], y: f32[8]) -> f32 = dot(x, y) # emits dot__8

Shape parameters can take an explicit kind: [N: nat] is the same as [N]. Multiple shape parameters are comma-separated:

fn matshape[M, K, N](
a: f32[M, K], b: f32[K, N]
) -> f32 = 0.0

We come back to shapes in detail in Tensors.

Next: Tensors