Appearance
Domains
A score is not just an integer. A player ID is not just an integer either. When you treat them as their raw types, mistakes happen: you pass a score where a player ID was expected, or an ID where a count belongs.
Domains solve this by wrapping a carrier type with a semantic name and its own operations. The compiler prevents you from mixing them up.
aivi
domain Score over Int
domain PlayerId over Int
domain Tag over Text
value highScore : Score = 9000
value currentPlayer : PlayerId = 7
value label : Tag = "featured"You cannot pass a Score where a PlayerId is expected, even though both are backed by Int.
The standard library already ships Duration, Url, and Path as built-in domains — you do not need to declare those yourself.
Declaring a domain
aivi
domain Score over IntThis declares a Score domain whose runtime carrier is Int.
Literal suffixes
A domain can define integer suffix constructors:
aivi
domain Score over Int = {
suffix pts
type pts : Int
pts = n => Score n
}
value highScore : Score = 9000ptsSuffixes must be explicit and unambiguous. In current AIVI they must also be at least two characters long.
Operators and named members
Domains can attach operators and named methods directly under the declaration:
aivi
domain Score over Int = {
suffix pts
type pts : Int
pts = n => Score n
type (+) : Score -> Score -> Score
(+) = left right => left + right
}That lets you write domain-aware expressions such as:
aivi
value total : Score = 10pts + 5pts
value raw : Int = total.carrierCallable members use the same two-line pattern: annotate the member, then bind it.
aivi
domain Score over Int = {
type fromRaw : Int -> Score
fromRaw = raw => raw
}The body is checked against the carrier view of the domain, while callers still see the nominal signature.
For comparison, prefer Eq / Ord instances over authored domain operator members. Once a domain implements Ord.compare, ordinary <, >, <=, and >= work automatically for that domain.
self and receiver-style members
When a member operates on the current domain value, you can write it in receiver style with self:
aivi
domain Snake over List Cell = {
type fromCells : List Cell -> Snake
fromCells = cells => cells
type head : Cell
head = getOrElse (Cell 0 0) (listHead self)
type length : Int
length = listLength self
}fromCells stays explicit because it constructs a Snake from a carrier value. head and length use self, so their receiver is implicit in the annotation.
Generic domains
Domains can also be parameterised:
aivi
domain NonEmpty A over List AThis is useful when you want stronger guarantees than the carrier type alone can express.
The .carrier accessor
Every domain has a built-in .carrier accessor that returns the underlying carrier value at zero cost. You do not need to declare it — the compiler synthesizes it automatically:
aivi
domain Score over Int = {
suffix pts
type pts : Int
pts = n => Score n
}
value raw : Int = (100pts).carrierThis is useful when you need to pass a domain value to a function that expects the carrier type:
aivi
domain Snake over NonEmptyList Cell
value cells : List Cell = nelToList mySnake.carrierUnlike user-defined domain members, .carrier is always available on every domain without any declaration.
Summary
| Form | Meaning |
|---|---|
domain Name over Carrier | Declare a domain |
suffix pts + type pts : Int + pts = n => expr | Add an integer suffix constructor |
type (+) : D -> D -> D + (+) = x y => expr | Add an operator |
type member : T + member = x => expr | Add an authored callable member |
self | Implicit domain-typed receiver in authored bodies |
.carrier | Built-in accessor returning the carrier value (always available) |