Tagged Unions
Declaration
tagged Shape {
Circle { radius: f64; }
Rect { width: f64; height: f64; }
Point;
}A tagged union stores exactly one variant at a time, with a hidden discriminant tag. Variants with no fields use ; instead of {}.
Construction
let s = Shape.Circle { radius: 5.0; };
let r = Shape.Rect { width: 3.0; height: 4.0; };A construction names every payload field, like a struct literal:
a payload field can declare a default (Key { code: i64; mods: i64 = 0; }),
a last .. zeroes the rest (Shape.Rect { width: 3.0; .. }), and leaving
out any other field is a compile error. Each value must be of its field’s
type, as in a struct literal: Shape.Circle { radius: "big"; } is a compile
error (field 'radius' of Shape.Circle is f64, given string), so is
radius: 5 (write 5.0), and so is a field the variant doesn’t have.
A variant without fields is a value as it is: let p = Shape.Point;. A bare
Shape.Circle, which has fields, is a compile error that names them
('Shape.Circle' has fields (radius): ...), and so is a variant the union
does not have (tagged union 'Shape' has no variant 'Square').
Pattern matching
fn area(s: Shape) -> f64 {
match (s) {
Shape.Circle { radius } => { return 3.14159 * radius * radius; }
Shape.Rect { width; height } => { return width * height; }
Shape.Point => { return 0.0; }
}
}Match arms use Type.Variant { field_bindings } syntax. Fields are bound by name. Use .. to ignore remaining fields, or : alias to rename a binding.
Strings, structs and containers in variants
Variants can hold managed values — strings, structs with strings, containers, even the union itself through a container:
import collections from "collections";
pub tagged Value {
Null;
Num { n: f64; }
Str { s: string; }
Arr { items: Vec_Value; }
}
@tape { collections.gen_vec(Value); }gen_vec emits pub functions that take the element type, so Value must
be pub (see collections).
Such a union owns its payload: constructing one copies a variable into it (a freshly built value is moved in), assigning var b = a; deep-copies the current variant, reassigning frees the old payload, and the payload is freed when the union goes out of scope. Match bindings borrow the fields — copy one if it must outlive the arm. See Ownership.
Unions from other modules
A pub tagged union of another module is named through the import, in
every position:
import json from "json";
let num: json.Value = json.Value.Number { v: 2.5 };
let n: json.Value = json.Value.Null;
match (num) {
json.Value.Number { v } => { io.println("{v}"); }
_ => {}
}
if (n is json.Value.Null) { io.println("null"); }Its pub methods are called as usual: num.as_f64().
Type narrowing with is
if (s is Shape.Circle) {
io.println("{s.radius}"); // narrowed: the Circle's fields are readable
}s is Shape.Circle tests which variant is active. Inside the if or
while body it guards, s is narrowed to that variant and its fields can
be read directly, methods included: with a names: Vec_string field,
m.names.len() and m.names.get(0) work as they do on a match arm’s
binding.
Untagged unions (t0/t1 only)
union Register {
i: i64;
f: f64;
bytes: [8]u8;
}Like C’s union — all fields occupy the same memory, no discriminant tag. You’re responsible for knowing which interpretation is valid. Only available in t0/t1 profiles; no managed types (strings, slices, GC pointers) allowed inside. Use tagged unions for the safe “one of several types” case.
Last modified: