Skip to content

Functions & Lambdas

A function names a piece of computation; a lambda is the anonymous version of the same thing. Both share the same call shape and the same parameter conveniences — defaults, named arguments, irrefutable destructuring — and both slot interchangeably into pipes and higher-order code.

fn name(params): ReturnType { body }. The body is a block expression: its last expression is the return value — no return keyword needed for the happy path. return exists for early exits from inside a conditional:

fn add(x: Int, y: Int): Int {
  x + y
}

fn abs(x: Int): Int {
  if x < 0 { return -x }
  x
}

fn main(): Int {
  dbg add(3, 4)
  dbg abs(-9)
}

Lambdas are written |params| body. Annotate parameter types when there’s no other clue; otherwise let inference handle it. A lambda is a first-class value — bind it to a name, pass it to another function, or return one.

fn main(): Int {
  inc = |x: Int| x + 1
  dbg inc(41)
}

Named functions are first-class too: f = add binds the value of add to f, and f(3, 4) works exactly like add(3, 4).

Within a scope, a function name has one meaning. Nomi does not choose between multiple same-named functions by argument type:

type UserId String

fn parse(text: String): Int {
  0
}

fn parse(id: UserId): Int {
  0
}

fn main() {
  Unit
}

Use names that describe the conversion or construction path: Int.parse(text), Date.parse(text), Date.from_parts(...), String.from_codepoints(...). For polymorphism, define an interface and implement it for each type; dispatch is explicit at the call site with a type or interface qualifier.

Any parameter — in a function or a lambda — can carry a default. Callers can omit it, or pass a value to override.

fn greet(name: String, greeting: String = "Hello"): String {
  "${greeting}, ${name}!"
}

fn main(): Int {
  dbg greet("World")
  dbg greet("World", "Hi")

  // Lambdas take defaults the same way:
  add = |x: Int, y = 10| x + y
  dbg add(5)
  dbg add(5, 2)
}

Any parameter can be passed by name at the call site, in any order. Positional and named arguments can mix — positional first, then named. Named arguments make defaulted middle parameters easy to override without remembering positions:

fn connect(host: String, port: Int = 8080, timeout: Int = 30): String {
  "${host}:${port}:${timeout}"
}

fn main(): String {
  dbg connect("localhost")
  dbg connect("localhost", timeout: 10)
  dbg connect(host: "api", port: 443, timeout: 5)
}

A function or lambda parameter can take any irrefutable pattern in place of a plain name — tuple, anonymous struct, distinct-type wrapper, single-variant enum. The pattern unpacks the argument on the way in.

A pattern whose head names a concrete type (a distinct constructor like Dur(...), a typed struct like Point{...}) is self-typing — the type is in the pattern, so no : Type annotation needed. A type-less pattern (a plain tuple, an anonymous struct) needs the annotation:

type Dur Int

struct Point {
  field x: Int
  field y: Int
}

fn subtract(Dur(a), Dur(b)): Dur {
  Dur(a - b)
}

fn sum_point(Point{x, y}): Int {
  x + y
}

fn add_pair((a, b): (Int, Int)): Int {
  a + b
}

fn main(): Int {
  Dur(diff) = subtract(Dur(10), Dur(3))
  dbg diff
  dbg sum_point(Point{x: 4, y: 5})
  dbg add_pair((20, 22))
}

When a function’s last parameter is a lambda, the call site can leave the lambda unnamed at the end and it routes into the last slot automatically — even when middle defaulted parameters are overridden by name. The pattern keeps callback-style functions readable:

fn transform(x: Int, factor: Int = 1, f: (Int) -> Int): Int {
  f(x * factor)
}

fn main(): Int {
  dbg transform(5, |x| x + 1)
  dbg transform(5, factor: 3, |x| x + 1)
}

The next chapter — Pipes — shows how |> weaves everything in this chapter into expressions that read top-to-bottom.