Records & Data
Hale models structured data with nominal type records, tagged enum unions, and type-tagged set unions (A | B | C). Every data type is checked at compile time, copied by value with automatic copy-on-write optimization, and matched exhaustively with match.
type Book { id: Int; title: String; }type Trade { qty: Int; px: Decimal<2>; }type Inbound = Book | Trade;
fn describe(msg: Inbound) -> String { return match msg { Book(b) -> f"book #{b.id}: {b.title}", Trade(t) -> f"trade {t.qty} @ {t.px}", };}
fn main() { let m: Inbound = Trade { qty: 10, px: 42.50d }; println(describe(m));}Data Declarations Overview
Section titled “Data Declarations Overview”| Construct | Syntax Example | Description |
|---|---|---|
Record (type) |
type Player { id: String; score: Int = 0; } |
Fixed-layout product type with optional field defaults |
Enum (enum) |
type Event = enum { Tick(Int), Halt }; |
Tagged union with unit or payload-carrying variants |
Set union (|) |
type Inbound = Book | Trade | Unknown; |
Union of existing struct/enum types where the member type is the tag |
| Type alias | type Cents = Int; / type IntBox = Box<Int>; |
Transparent synonym for an existing type or concrete generic instantiation |
| Generic record | type Box<T> { value: T = 0; } |
Template monomorphized per concrete type argument (Box<Int>) |
| Fixed array / slice | [Int; 8] / [Player] |
Inline fixed-length array ([0; 8]) or slice view |
| Tuple | let pair = (1, "one"); |
Lightweight anonymous grouping |
Records (type)
Section titled “Records (type)”Declare a record at the top level of a file with type Name { ... } and construct it with a struct literal:
type Config { host: String = "127.0.0.1"; port: Int = 8080;}
fn main() { let c = Config { port: 9000 }; // `host` uses its default "127.0.0.1" println(c.host, ":", c.port);}- Compile-time field verification: Unknown field names, missing required fields (fields without defaults), or mismatched field types fail compilation at the literal—including for cross-seed records constructed as
alias::Config { ... }. - Top-level only:
typedeclarations must appear at the top level outside anylocusbody. - No loci inside records: Because records are pure values with no lifecycle hooks, a
typefield (orenumvariant) cannot hold alocusor a function pointer whose signature names a locus (fn() -> Worker). Store loci inside another locus’sparamsinstead. (A record field typed by aninterfaceis permitted because it is a non-owning reference.)
Value Copy Semantics and Mutation
Section titled “Value Copy Semantics and Mutation”Independent Value Copies and Copy-on-Write
Section titled “Independent Value Copies and Copy-on-Write”Binding a record, String, Bytes, tuple, or array from a field or variable (let saved = self.row; or copy = saved;) gives the binding an independent logical copy of the value:
let saved = self.row;self.row = Row { }; // `saved` retains the previous valuelet mut copy = saved;copy.score = 9; // `saved.score` is unchangedUnder the hood, the compiler avoids copying whenever no mutation can alias the read, and performs a copy on the first write into a local derived from a field, match payload, for element, or tuple.
In-Place Read-Modify-Write Optimization
Section titled “In-Place Read-Modify-Write Optimization”To update a nested array or field inside a locus, either assign directly (self.row.scores[0] = 9;) or read into a local, mutate, and write it back:
let mut scores = self.row.scores;scores[0] = 9; // `self.row.scores` is unchanged until written backself.row.scores = scores; // optimized to an in-place write with zero allocationsWhen a local read from a field is mutated and written back to the same field with no intervening reads of that field, the compiler updates the field’s storage in place without allocating or copying.
Reference Semantics for Parameters, @form(vec) Iteration, and Locus Handles
Section titled “Reference Semantics for Parameters, @form(vec) Iteration, and Locus Handles”| Target | Mutation Behavior |
|---|---|
Record function parameter (fn bump(r: Row)) |
Passed by reference to the caller’s value; r.scores[0] = 9; mutates the caller’s record. |
@form(vec) loop variable (for x in v.items) |
Visits the vec’s cells in place; writing through x updates the stored element. |
Locus field via handle (l.name = s;) |
Copies s into locus l’s arena and recycles the replaced value so repeated external writes never grow l’s arena. |
A method that writes into a record parameter (fn put(x: Bag, p: Bytes) { x.b = p; }) writes the caller’s record, so the value is kept as long as that record, never only for the call. The method may pass the record on to another writer, directly or through a block, if or match, and the same holds at every step. A record a field of self holds (put_free(self.bag, p)) is kept in the locus’s arena. One call can hand a writer only one owner’s records. self.put_two(self.held, x, p), or self.put(if c { self.held } else { x }, p), mixes the locus’s record with the caller’s and is rejected. Copy one side into a local record first (let mine = Bag { b: x.b };) and hand that, or write the held record through self.
Such a write is not recycled when the method returns, and does not replace the old value in place the way self.bag.b = p does. Handing a writer self.bag once per message, or once per pass of a run() loop, keeps every value until the locus dissolves, and hale check reports the call as an unbounded allocation. Write the held record through self instead, or hand the writer a record the loop body builds.
Arrays ([T; N]) and Tuples
Section titled “Arrays ([T; N]) and Tuples”Use [T; N] for fixed-length inline arrays, [T] for slice views, and (a, b) for small ad-hoc tuples:
type Match { players: [Player]; } // slice field
let xs = [1, 2, 3]; // [Int; 3]let zeros = [0; 8]; // [Int; 8] filled with 0let pair = (1, "one"); // tupleFor growable lists, use a @form(vec) locus or bounded[T; N] (Lists & Maps).
Type Aliases (type Name = Target;)
Section titled “Type Aliases (type Name = Target;)”A type alias introduces a transparent synonym for an existing type:
type Cents = Int;
fn price_of(n: Cents) -> Cents { return n * 2; }
fn main() { let c: Cents = 250; println(price_of(c));}Because Cents is transparent, it is completely interchangeable with Int (including in struct construction RowAlias { id: 1 } and enum patterns LightAlias::Red). If you need the compiler to reject mixing two integer types, use distinct Int (Units, End to End) or a single-field struct (type Cents { v: Int; }).
Type Alias Rules
Section titled “Type Alias Rules”- Literal construction requires a record or locus target: If
type Cents = Int;, writingCents { }is a compile error. - No cyclic aliases:
type A = B; type B = A;is rejected. - Concrete generic aliases only: Generic aliases (
type Twin<T> = Pair<T>;) are not supported, but aliasing a concrete instantiation (type IntBox = Box<Int>;) is supported and supplies the type arguments forlet b: IntBox = Box { value: 1 };(for both generic records and generic loci).
Enums (enum) and Exhaustive match
Section titled “Enums (enum) and Exhaustive match”An enum defines a tagged union whose variants may be unit tags or carry positional payloads:
type Event = enum { Tick(Int), Trade(Decimal, Int), Halt,};
fn handle(e: Event) { match e { Event::Tick(0) -> println("tick zero"), Event::Tick(n) -> println("tick #", n), Event::Trade(price, size) -> println("trade ", size, " @ ", price), Event::Halt -> println("halt"), }}
fn main() { handle(Event::Tick(0)); handle(Event::Trade(19.99d, 5));}- Construction and formatting: Construct variants with
EnumName::Variant(Event::Halt,Event::Tick(3)). Printing an enum outputs its declared name and payload (Event::Tick(3)), even when imported via a library alias. - Exhaustive matching:
matcharms can match literal sub-patterns (Event::Tick(0)) before variable bindings (Event::Tick(n)), and must cover every variant in theenum. enumvs.fallible(E): Useenumfor domain states and sum-type messages; usefallible(E)(When a Call Can Fail) for functions that can fail.
Set Types (A | B | C)
Section titled “Set Types (A | B | C)”When each alternative is already a standalone type record or enum, declare a set type using | instead of wrapping each record in a redundant enum variant:
type Book { id: Int; title: String; }type Trade { qty: Int; px: Float; }type Unknown { raw: String; }type Inbound = Book | Trade | Unknown;
fn decode(n: Int) -> Inbound { if n == 0 { return Book { id: 7, title: "Kim" }; } if n == 1 { return Trade { qty: 2, px: 1.5 }; } return Unknown { raw: "?" };}
fn describe(m: Inbound) -> String { return match m { Book(b) -> "book " + b.title, Trade(t) -> "trade of " + to_string(t.qty), Unknown(u) -> "unknown " + u.raw, };}- Type-as-tag coercion: A
Book,Trade, orUnknownvalue automatically coerces toInboundwhereverInboundis expected (return values, function arguments, typedletbindings, or struct fields). - Target type required for mixed branches: An unannotated
iformatchwhose arms produceBookandTrade(let m = if n == 0 { Book { ... } } else { Trade { ... } };) fails typechecking because the compiler does not guess which set type joins them; annotate the binding (let m: Inbound = ...) or return from a function declared-> Inbound. - Members must be declared structs or enums: Primitive types like
Intcannot appear directly in a set; wrap them in a struct (type Count { n: Int; }). - Wire representation and seams: Across JSON APIs, a set serializes as a tagged object keyed by the member’s type name (
{"Trade": {"qty": 2, "px": 1.5}}). At the service layer, a seam can dispatch each member of a set to a dedicated handler method and verify exhaustiveness at compile time.
Generic Records and Loci (<T>)
Section titled “Generic Records and Loci (<T>)”Both type records and locus declarations can be parameterized by type arguments:
type Box<T> { value: T = 0;}
locus Cache<K, V> { params { cap: Int = 1; }}
fn main() { let b: Box<Int> = Box { value: 1 }; let c: Cache<Int, String> = Cache { cap: 2 }; println(b.value, " ", c.cap);}Monomorphization and Type-Argument Rules
Section titled “Monomorphization and Type-Argument Rules”- Explicit type context required: In
let b: Box<Int> = Box { value: 1 };, the struct literal spells the template nameBoxwhile the concrete type argument<Int>comes from the surrounding type context (aletannotation, function return type-> Box<Int>, enclosing struct field, assignment target, or[Box<Int>; 2]array annotation). Writinglet b = Box { value: 1 };without a type annotation is a compile error. - Supported type arguments: Any user-defined
typerecord and all built-in types (Int,Float,Bool,String,Duration,Decimal,Time,Bytes,BytesView,BytesMut,StringView) are valid type arguments. (Uinthas no lowered representation and is rejected; useInt.) - Name mangling: Each instantiation compiles to a distinct specialized type named
Template_Arg1_Arg2(such asBox_IntorCache_Int_String). InsideBox<T>, the identifierTalways resolves to the type parameter even if the seed also declares a top-level type namedT.
Next: Files.