Variants
A variant declares a type whose value is exactly one of a closed list of cases, and each case may carry data of its own. A thermometer reading is missing, or an exact temperature, or a range between two: three cases, two of them with numbers. A struct holds all of its fields at once; a variant holds one case at a time, and only that case's data.
variant = [ attributes ] [ "pub" ] "variant" name [ type-parameters ] "{" [ case { "," case } ] "}"
case = name // unit
| name "(" type { "," type } ")" // positional
| name "{" { name ":" type ";" } "}" // named
variant Reading {
Missing,
Exact(float64),
Between { low: float64; high: float64; }
}
Cases are separated by commas, with no comma after the last. Named fields inside a case end with ;, as struct fields do.
| Case | Shape | Carries | Built as |
|---|---|---|---|
Missing | unit — a bare name, like an enum case | nothing | Reading::Missing |
Exact | positional — values by position | one float64 | Reading::Exact(21.5) |
Between | named — fields by name | low and high | Reading::Between { low: 20.0, high: 24.0 } |
A positional case may carry any number of values, Jump(int, int). The three shapes may be mixed freely in one variant.
Building a case
A case is built through its type's name, with the syntax of its shape: nothing for a unit case, call syntax for a positional one, struct-literal syntax for a named one. The fields of a named case may come in any order, and every field must be given:
func Measure(low: float64, high: float64) -> Reading {
if low > high {
return Reading::Missing;
}
if low == high {
return Reading::Exact(low);
}
return Reading::Between { low: low, high: high };
}
A unit case may also be written with empty parentheses, Reading::Missing(). The payload is checked against the case's declaration like a call's arguments:
error: argument 1 to variant case 'Reading::Exact' has type 'int', but field 1 requires 'float64'
error: initializer for 'Reading::Between' is missing required field 'high'
rux 0.4.0 does not check this yet: a named case can be built, and matched, positionally, as in
Reading::Between(20.0, 24.0). Use the field names.Reading the data
A variant has no fields of its own to read: the data belongs to one case, and which case is held is known only at run time. reading.0 is refused with type 'Reading' has no field '0'. The data is taken out with match, which tests the case and binds its payload in one step:
func Describe(reading: Reading) -> float64 {
return match reading {
.Missing => 0.0,
.Exact(value) => value,
.Between { low, high } => (low + high) / 2.0
};
}
Equality
Two variant values are equal when they hold the same case and that case's payloads are equal, compared in declaration order. Different cases are never equal, whatever they carry:
let precise = Reading::Exact(21.5);
let same = precise == Reading::Exact(21.5); // true
let other = precise == Reading::Missing; // false
Every payload type of every case must support ==. A variant with a case carrying a char8[..], which has no ==, cannot be compared at all:
error: variant equality for 'Token' is unavailable because payload type 'char8[..]' in case 'Token::Word' has no '==' operator
Variants have no built-in ordering.
Generic variants
A variant may take type parameters, used in its payloads:
variant Lookup<T> {
Found(T),
Missing
}
A case with a payload infers the type arguments from it: Lookup::Found(5) is a Lookup<int>. A case without one takes them from the type it is assigned, passed or returned as, or names them itself:
let hit = Lookup::Found(5); // Lookup<int>
let miss: Lookup<int> = Lookup::Missing;
let empty = Lookup::Missing<int>();
error: variant case 'Lookup::Missing' requires 1 type argument, but 0 were provided
help: annotate the destination, as in 'let value: Lookup<...> = Lookup::Missing;', or write 'Lookup::Missing<...>()'
An annotation wins over the payload: let small: Lookup<uint8> = Lookup::Found(7); makes the 7 a uint8. See Generic types.
The tag is private
A variant value stores a tag that says which case it holds, followed by storage for the largest case. The tag belongs to the compiler: a variant has no backing type and no case values, and it does not convert to or from an integer.
| Written | Error |
|---|---|
variant Code: uint8 { A, B } | variant 'Code' cannot specify a base type |
variant Numbered { A = 1, B } | variant 'Numbered' cannot assign case discriminants |
precise as int | cannot cast variant 'Reading' to scalar type 'int' |
The tag's numbers are not part of any contract and must never be written to a file or sent to another program. Data that crosses a program's boundary as numbers is encoded with an enum, whose values are chosen in the source, and translated to and from the variant.
A variant is copyable when every payload is, and is moved otherwise; destroying one destroys only the active case's payload. See Copy and move.
Recursive variants
A variant cannot contain itself by value, but a case may hold a pointer to another value of the same variant:
variant Tree {
Leaf(int),
Node { left: *Tree; right: *Tree; }
}
Variants, enums, structs and unions
| Declaration | Holds | Data per alternative | Which one is held is… |
|---|---|---|---|
struct | every field at once | — | not a question: all are |
enum | one case | none | its integer value, chosen in the source |
variant | one case | any | a private tag, checked by match |
union | one member's bytes | any | not recorded; the program must know |
See also
- Matching variants — patterns, bindings and exhaustiveness
- Enums — cases without data, with integer values
- Sum types — a value of one of several types, without case names
- Unions — overlapping storage without a tag
- Learn: Variant, Variant match, Generic type
Overview
Scalar enumerations: declaring enum cases, the backing integer type and explicit values, naming cases, as conversions in both directions, and matching.
Matching
Taking a variant apart with match: case patterns for unit, positional and named cases, binding payloads, nesting, guards and exhaustiveness.