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.

CaseShapeCarriesBuilt as
Missingunit — a bare name, like an enum casenothingReading::Missing
Exactpositional — values by positionone float64Reading::Exact(21.5)
Betweennamed — fields by namelow and highReading::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'
Named cases take named fields.
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.

WrittenError
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 intcannot 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

DeclarationHoldsData per alternativeWhich one is held is…
structevery field at once—not a question: all are
enumone casenoneits integer value, chosen in the source
variantone caseanya private tag, checked by match
unionone member's bytesanynot recorded; the program must know

See also