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.
Nomi at a glance
Section titled “Nomi at a glance”- Statically typed, with local inference — named functions spell out
parameters and every non-
Unitreturn type; bindings and lambdas infer. - Fully immutable — bindings, structs, and collections are all
persistent; “modifying” returns a new value. Repetition flows through
Iterpipelines,Iter.loop, and recursion with tail-call optimization. - Functional-first and pattern-matching — first-class functions, algebraic
data types (structs + tagged enums), exhaustive
casedestructuring. - Pragmatic local control flow —
return,break,continue, and prefixtryare 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 withderive, 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)orDisplay.to_string(value), never value-dotvalue.rename(). - Pipe-oriented —
x |> f()reads top-to-bottom; most non-trivial code flows as a pipeline. - Structured concurrency —
concurrent { ... }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 = mockoverrides. - Built-in tests — named
test/testsblocks, assertions, setup, and//!attached examples are ordinary Nomi code run bynomi test.
Where Go fits
Section titled “Where Go fits”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.
Influences
Section titled “Influences”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.
Coming from another language
Section titled “Coming from another language”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.
A taste
Section titled “A taste”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.