Structs, Enums, Distinct Types
Nomi gives you three keyword families for declaring your own data:
struct— records with named fields, one field line at a time.type— distinct wrappers around an existing type (type Id Int) and tag-only “bare” types with no payload (type Expired).enum— sum types (a value that’s exactly one of several variants), declared one variant line at a time.
Declaration bodies are newline-separated item lists — no commas between
fields, no separators between variants. (Construction literals and anonymous
struct types keep their commas.) Struct bodies contain field items plus the
type’s own functions and once values; enum bodies contain variant items plus
the type’s own functions and once values; type / extern type bodies hold
only the type’s own functions and once values. Derives live beside the type as
derive Interface for Type declarations. Manual interface implementations live in sibling
impl Interface for Type { ... } blocks —
that’s the subject of Interfaces & Dispatch.
Type declarations do not contain nested declarations, but a type can qualify
related sibling types with a dotted PascalCase name:
type Day Int type Day.Hours Int
Day.Hours is a nominal type in its own right. Its constructor,
destructuring pattern, impl blocks, derives, and interface implementations use
the full name (Day.Hours(48), Day.Hours(n) = hours,
impl Display for Day.Hours { ... }). Use dotted types for domain names that
belong under a real type; put broader API hierarchy in a module.
We’ll go in that order. The types you declare in this chapter don’t yet
opt in to Display, so we’ll use dbg to inspect them with Nomi’s
universal Debug rendering; opting in to IO.print waits for
Interfaces & Dispatch.
Structs
Section titled “Structs”A struct is a record with named fields. Construct with
TypeName{field: value, …}, access fields with .field. Fields can carry
default values:
struct User {
field name: String
field age: Int = 0
}
fn main(): User {
alice = User{name: "Alice", age: 30}
dbg alice.name
dbg alice.age
// The default lets the caller omit `age`.
bob = User{name: "Bob"}
dbg bob.age
// Field-name punning: `User{name, age}` is shorthand for
// `User{name: name, age: age}` when bindings of those names are in scope.
name = "Carol"
age = 28
carol = User{name, age}
dbg carol
}
Type Functions And once Values
Section titled “Type Functions And once Values”A type body also owns that type’s inherent API. Put fields or variants first;
then add type-qualified once values and functions that belong to the type
itself. Manual interface implementations and derives stay beside the type,
not inside it.
pub struct RetryPolicy {
field retries: Int
pub once default_retries = 3
pub fn default(): RetryPolicy {
RetryPolicy{retries: RetryPolicy.default_retries}
}
pub fn exhausted?(policy: RetryPolicy): Bool {
policy.retries == 0
}
}
fn main(): Bool {
policy = RetryPolicy.default()
dbg RetryPolicy.default_retries
dbg policy
RetryPolicy.exhausted?(RetryPolicy{retries: 0})
}
Anonymous structs
Section titled “Anonymous structs”When you want a quick record without declaring a named type, drop the type
name and write the struct literally. The value carries its own structural
type — {x: Int, y: Int} here — and field access works the same way:
fn main(): Int {
point = {x: 10, y: 20}
dbg point
dbg point.x + point.y
}
Distinct types
Section titled “Distinct types”type Name UnderlyingType declares a distinct type that wraps an existing
one. The two share a runtime representation but the compiler keeps them
separate — a function taking Id will refuse a plain Int, even though
both use the same representation. This catches whole categories of
domain bugs at compile time:
type Id Int
type Email String
fn main(): Int {
id = Id(42)
dbg id
// To pull the inner value out, cast or destructure-bind:
raw = Int(id)
dbg raw
Id(again) = id
dbg again
}
Bare types
Section titled “Bare types”A type declaration with nothing after the name declares a zero-sized
“bare” type — just a tag, no payload. Useful as a sentinel value or as a
no-data variant when embedded in an enum (see the deep-dive below), and as the
stateless adapter shape when a module-like API needs a value that can implement
an interface:
type Expired
type Online
fn main(): Online {
// Bare types are constructed by name — no parens, no fields.
state = Expired
dbg state
dbg Online
}
An enum is a value that’s exactly one of a fixed set of variants. Variants can be bare (no payload), positional (a single anonymous field), or struct-shaped (named fields):
enum Direction {
variant North
variant South
variant East
variant West
}
enum Shape {
variant Circle Float
variant Rectangle {width: Float, height: Float}
}
fn main(): Shape {
dbg Direction.North
c = Shape.Circle(3.0)
dbg c
r = Shape.Rectangle{width: 4.0, height: 5.0}
dbg r
}
Dot-leading shorthand
Section titled “Dot-leading shorthand”The fully-qualified Direction.North / Shape.Circle(...) form
always works. When the expected type is known — most commonly
inside a case whose subject is a known enum, but also a binding
annotation, a function parameter, or a return value — you can drop
the enum name and use the dot-leading shorthand:
enum Direction {
variant North
variant South
variant East
variant West
}
fn describe(d: Direction): String {
// `d` is a Direction, so each arm's dot-leading pattern
// unambiguously matches one of Direction's variants.
case d {
.North -> "up"
.South -> "down"
.East -> "right"
.West -> "left"
}
}
fn main(): String {
// `describe` expects a Direction, so `.North` means `Direction.North`.
dbg describe(.North)
dbg describe(.West)
}
This previews case, which Pattern Matching covers in full — but
the dot-leading rule is the same in any position the compiler can
pin the type: pattern arms, function arguments, return values, list
element types (walk: List<Direction> = [.North, .East]), annotated
bindings (d: Direction = .North).
Going deeper
Section titled “Going deeper”Embedding existing types
Section titled “Embedding existing types”When a variant’s payload would be a type you’ve already defined as a
standalone struct, distinct type, or bare type, declare it with embeds
instead of repeating the shape inline. The embedded type stays
independently usable, and values of that type flow into the enum
without a wrapping constructor:
struct Click {
field x: Int
field y: Int
}
struct KeyDown {
field key: String
}
type FocusLost // bare — zero-sized
enum Event {
variant embeds Click
variant embeds KeyDown
variant embeds FocusLost
}
fn main(): List<Event> {
// Each event constructed standalone — no `Event.Click{...}` wrapping.
// Structs use `{...}`; the bare type is just its name.
events: List<Event> = [Click{x: 10, y: 20}, KeyDown{key: "Enter"}, FocusLost]
dbg events
}
The benefit is subtype coercion: a Click value flows into any
Event-typed slot (list elements, function arguments, return values)
without explicit construction. Pattern matching destructures embedded
structs the same way as struct variants — case e { Event.Click{x, y} -> … } —
shown in Pattern Matching.
embeds is Nomi’s replacement for the OO extends pattern: a
List<Node> of supervision-tree members (Worker, Supervisor,
DynamicSupervisor) is an enum with three embeds declarations
rather than a class hierarchy.
Type aliases
Section titled “Type aliases”A typealias is a transparent synonym for an existing type — the alias
and the original are fully interchangeable, no wrapping or conversion. The
payoff is making complex generic signatures readable:
typealias Index Map<String, List<Int>>
fn record(index: Index, bucket: String, n: Int): Index {
existing: List<Int> = case Map.get(index, bucket) {
Some(xs) -> xs
None -> []
}
Map.put(index, bucket, [n, ..existing])
}
fn main(): Map<String, List<Int>> {
start: Index = Map.empty()
result =
start
|> record("evens", 2)
|> record("evens", 4)
|> record("odds", 1)
dbg result
}
record’s signature reads (Index, String, Int) -> Index instead of
the noisier (Map<String, List<Int>>, String, Int) -> Map<String, List<Int>>.
Index is literally Map<String, List<Int>> — call sites pass
plain map literals, and result is the same type whether you spell
it Index or the full generic.
Use type when you want a new type the compiler distinguishes from
its representation (UserId shouldn’t accidentally be passed where
OrderId is expected — both are represented as Int, but the wrapper
keeps them apart). Use typealias when a complex type expression has
a meaningful name and writing it out everywhere clutters signatures.