Skip to content

The Nomi Language Tour

A guided, interactive introduction to Nomi. Runnable examples are complete programs you can edit on the page: change a value and the output below re-runs automatically. When the program is valid and you pause editing, the example formats itself. Hover any name for the same signature and documentation your editor shows. The output panel shows the program’s current result. Nomi is still pre-release; for now, the interactive examples on this site are the main way to try the language.

  • Statically typed, with local inference — named functions spell out parameters and every non-Unit return type; bindings and lambdas infer.
  • Fully immutable — bindings, structs, and collections are all persistent; “modifying” returns a new value. Repetition flows through Iter pipelines, Iter.loop, and recursion with tail-call optimization.
  • Functional-first and pattern-matching — first-class functions, algebraic data types (structs + tagged enums), exhaustive case destructuring.
  • Pragmatic local control flowreturn, break, continue, and prefix try are ordinary tools when they make the path through a function clearer.
  • Explicit interfaces — types implement interfaces with impl Interface for Type { ... } blocks, opt into structural synthesis with derive, and use qualified calls for dispatch.
  • Functions, not methods — behavior lives in modules, type bodies, and interfaces; calls use qualified prefix form like User.rename(user) or Display.to_string(value), never value-dot value.rename().
  • Pipe-orientedx |> f() reads top-to-bottom; most non-trivial code flows as a pipeline.
  • Structured concurrencyconcurrent { ... } blocks, tasks, channels, and context-style cancellation show the intended shape for scoped concurrent work. This surface is still earlier than the core type/expression system.
  • Typed app fields — runtime context, configuration, and dependencies live on a concrete app value; tests can swap dependencies with scoped with MyApp.field = mock overrides.
  • Built-in tests — named test / tests blocks, assertions, setup, and //! attached examples are ordinary Nomi code run by nomi test.

Nomi is implemented in Go today: the interpreter, analyzer, LSP, stdlib host functions, and runtime are Go code, and the nomi CLI embeds that runtime. Some languages compile to Go source; Nomi does not. Today’s implementation walks Nomi source directly, and that same interpreter, built to wasm, runs the editable examples on this page in your browser.

The Go pairing is an implementation strategy, not Nomi’s language identity. Nomi gets a mature implementation substrate — garbage collection, cross-platform support, strong tooling, and practical runtime primitives — while keeping its own surface and semantics. The likely implementation path still stays Go-hosted, but the runtime architecture may move toward a bytecode VM as the language grows.

For most programs, that’s all you need to know about Go. Write Nomi, run with nomi, ship — the stdlib covers IO, collections, dates & times, JSON, concurrency, and the usual basics, and you won’t write any Go code.

Go shows up directly in two places today. The first is package experiments: a Nomi package is currently a directory carrying both nomi.toml (Nomi’s manifest) and go.mod (Go’s module file). Files are the import graph and visibility units inside that package; module declarations are named API containers within those files. Cross-package dependency management currently leans on Go tooling, but this is one of the less settled parts of the system.

The second is the concurrency vocabulary. Nomi has lightweight tasks, channels, context-style cancellation, and resource handling shaped by Go’s practical model, but task lifetime is structured: spawned work belongs to a concurrent { ... } block, and the compiler prevents tasks from leaking out of that scope.

When you need to call native code the stdlib doesn’t expose, extern fn is the bridge — declare the signature on the Nomi side, register the implementation on the Go side. See the FFI & Dynamic chapter.

Nomi sits closest to Gleam in day-to-day code: immutable data, Result/Maybe, ADTs, local inference, and small expression-shaped programs. It keeps Elixir’s pipeline-oriented reading style: transformations flow top-to-bottom, pattern matching is central, and immutable data is ordinary.

The interface and generic layer is more Rust-shaped: explicit impl Interface for Type { ... } declarations, where-based bounds, coherence, derives, and the orphan rule. Nomi does not have Rust’s ownership and borrowing system; values are immutable and managed by the runtime.

Go contributes the current implementation substrate, enforced formatting, channels, context-style cancellation, and a practical concurrency vocabulary. The exact package-management story is less settled. Nomi keeps the useful runtime instincts but puts structured-concurrency rails around them. The concurrent { } scoping rule is closer in spirit to the Trio/Kotlin family: spawned work cannot outlive the block that owns it.

Elixir — Pipes, pattern matching, immutable data, and small transformation-heavy functions transfer well. The big shifts are static types, declared ADTs instead of atoms/tagged tuples, Iter pipelines instead of protocol-driven Enum/Stream, pragmatic return / break / continue control flow, and structured concurrent { } blocks instead of actors and supervision trees.

Gleam — The closest reading experience for many people: expression-shaped code, ADTs, Result/Maybe, no null, and local type inference. Nomi differs with bare bindings (no let), explicit impl Interface for Type blocks, derive declarations, return / break / continue, app fields and resources, typed literals, and a currently Go-hosted runtime/tooling story.

Rust — Traits map to interfaces, conformances use impl Interface for Type { ... }, where bounds are checked at call sites, Result/Maybe use prefix try, and distinct types fill the newtype role. The differences are just as important: no ownership or lifetimes, no mut, no methods or value-dot calls, no macros, and inference stays local to function bodies.

Go — The implementation and some tooling are Go-shaped today: go.mod, go get, strong formatting, explicit imports, channels, context-style cancellation, and qualified calls like IO.print(value). The language model is not Go’s: values are immutable, errors are Result/sum types instead of (value, err)/nil, interface conformance is explicit, and concurrency is structured rather than fire-and-forget. Package management and runtime architecture remain active design areas.

Here’s idiomatic Nomi together. We’ll unpack each piece across the tour:

import std/io: IO

enum Shape {
  variant Circle Float
  variant Rectangle {width: Float, height: Float}
}

//! assert area(.Circle(3.0)) == 28.27431
fn area(s: Shape): Float {
  case s {
    .Circle(r) -> r * r * 3.14159
    .Rectangle{width, height} -> width * height
  }
}

fn main() {
  [
    Shape.Circle(2.0),
    Shape.Rectangle{width: 3.0, height: 4.0},
    Shape.Circle(1.0),
  ]
  |> Iter.map(area)
  |> Iter.each(|s| IO.print(s))
}

Tagged enums with mixed positional + struct variants, a //! attached test, case pattern matching that destructures the payload, stacked pipes, and dispatch through qualified calls — a compact pass through Nomi’s everyday shape.

Bindings & Expressions is the place to start; each chapter builds on the last.