Structs, Enums, Distinct Types
Nomi gives you these building blocks for declaring and grouping your own data:
struct— records with named fields, one field line at a time.tuple— fixed positional groupings for small local shapes.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: struct fields and enum variants do not use commas. Construction literals still do.
We’ll start with named and anonymous records, then tuples, wrappers, tags, and
sums. The examples use dbg so you can inspect values before the next chapter
introduces Interfaces & Dispatch and
Display.
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 {
name: String
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
}
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
}
Tuples
Section titled “Tuples”Tuples group a fixed number of values by position instead of by field name. They are handy for small, local pairings where the positions are obvious. Destructure them with a tuple pattern:
fn main(): Int {
pair = ("Ada", 37)
(name, score) = pair
dbg pair
dbg name
score
}
Use a struct when the grouped values deserve names at the boundary of an API.
(String, Int) is fine while the meaning is local; User{name: String, score: Int} is clearer once the shape starts traveling.
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 for
stateless adapters that need a real value to 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 {
North
South
East
West
}
enum Shape {
Circle Float
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 {
North
South
East
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).
Type-Owned Functions
Section titled “Type-Owned Functions”Named types can own helper functions in an impl Type { ... } block. Nomi
does not have value.method() syntax; the owner stays visible at the call site:
User.full_name(user), String.trim(text), List.concat(xs, ys).
struct User {
first: String
last: String
}
impl User {
fn full_name(user: User): String {
"${user.first} ${user.last}"
}
fn rename(user: User, first: String): User {
User{first, last: user.last}
}
}
fn main(): String {
alice = User{first: "Ada", last: "Lovelace"}
dbg User.full_name(alice)
renamed = User.rename(alice, "Augusta")
dbg User.full_name(renamed)
User.full_name(renamed)
}
Use impl Type for operations whose natural home is a real value type:
constructors, projections, validations, conversions, and transformations. The
same shape works for structs, enums, distinct types, opaque types, host types,
and generic types such as impl Box<T> { ... }.
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 {
x: Int
y: Int
}
struct KeyDown {
key: String
}
type FocusLost // bare — zero-sized
enum Event {
embeds Click
embeds KeyDown
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<Event> of mixed UI event values is an enum with one embeds
declaration per concrete event shape 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 a Map of List
values — 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.