Skip to content

Enums & Patterns

Jiang uses enum for a fixed set of named cases. A case can also carry values, which makes the enum an algebraic data type.

enum Mode {
    read,
    write,
}

Mode mode = Mode.read;
Mode other = .write;

Members may specify values:

enum Priority {
    low = 1,
    medium,
    high,
}

Enum case values use Int32 by default. Cases without an explicit value continue from the previous case.

Use enum [UInt8] or another integer type when the stored representation must be fixed:

enum [UInt8] ByteTag {
    block = 1,
    item,
}

UInt8 tag = UInt8(ByteTag.block);

Convert an enum value with a target integer constructor; enum cases do not expose a .value member:

Int value = Int(Mode.read);

For a no-payload integer enum, Type.init?(integer) returns the matching case or .none when the integer is not a declared case value:

Priority? priority = Priority.init?(2);
enum Outcome<T, E> {
    ok(T),
    err(E),
}

Outcome<Int, ParseErr> a = Outcome.ok(42);
Outcome<Int, ParseErr> b = .err(ParseErr.bad);

A case can carry more than one value. Names make those payload positions easier to document and bind:

enum Value {
    none,
    int_value(Int),
    pair(Int left, Bool right),
}

Payload cases can also have an explicit integer type and values:

enum [UInt8] WireValue {
    none = 1,
    int_value(Int) = 7,
}

UInt8 tag = UInt8(WireValue.int_value(42));

Cases must come before methods and nested types. Use ; to separate the cases from those members:

enum Value {
    none,
    int_value(Int);

    Bool has_value(self) {
        return self is .int_value(_);
    }
}

A payload follows the usual ownership rules. Moving the enum moves its active payload, borrowing a payload does not move it, and destroying the enum destroys only the payload of its current case.

if value is .int_value(_ n) {
    print(n);
} else {
    print(0);
}

Put ! after the name for a mutable inferred binding:

if value is .int_value(_ n!) {
    n = n + 1;
}

Use ref name to borrow an enum payload instead of binding it by value. The omitted type position means _; write ref T name when an explicit type pattern is useful:

enum Node {
    pair(Int, Int),
    empty,
}

Int read_ref(Int& value) {
    value$.get()
}

Int first(Node node) {
    if node is .pair(ref left, _) {
        return read_ref(left);
    }
    return 0;
}

This matters when the payload is large, non-copyable, or should be inspected without moving it. In patterns, ref! Int value creates a unique mutable Int&! borrow. Write ref Int value! or ref! Int value! when the resulting reference binding itself must be reassignable.

Optional values use .some(...) and .none:

if maybe is .some(payload) {
    print(payload);
} else {
    print(0);
}
Int code = switch mode {
    .read => 1,
    .write => 2,
}

Enum payloads:

Int result = switch value {
    .none => 0,
    .int_value(_ n) => n,
    .pair(left, right) => if right { left } else { 0 },
}

Borrowed enum payloads use the same ref sub-pattern:

Int result = switch node {
    .pair(ref Int left, _) => read_ref(left),
    .empty => 0,
}

Optional values:

Int value = switch maybe {
    .some(payload) => payload,
    .none => 0,
}

is and switch branch roots accept:

  • optional patterns: .some(payload) and .none
  • variant patterns: .name(...) or Type.name(...)
  • tuple patterns such as (left, right)
  • literal patterns such as null, numbers, chars, and booleans

Bindings and wildcards are sub-patterns. A Tuple payload is passed directly to its variant, so a (Int, Int) payload matches .pair(left, right). Preserve parentheses only for nested tuples: .nested((left, right), tail).