Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Enums and match

An enum is a type with a fixed set of named cases. A value of the enum holds exactly one of them. Use an enum where you would otherwise pass an integer code or a string, so the compiler checks that only the listed cases flow through, and that a match handles each one.

There are two kinds:

  • In an enum whose cases carry nothing (Red, Green), each case is an integer. Such an enum is a value type, like int.
  • When a case carries values (Circle(radius: int)), the enum is a reference type, like a struct. See Cases that carry values.
# An enum names a fixed set of cases; a slot of the enum type holds one.
enum Suit {
    Clubs,
    Diamonds,
    Hearts,
    Spades,
}

# A representation sets the stored integer type; a case may give its value.
enum Status: u8 {
    Ok,
    NotFound = 44,
    Failed,
}

struct Card {
    Suit suit;
    int rank = 1;
}

main(): int {
    var card = Card();
    assert(card.suit == Suit.Clubs, "a slot starts at the case whose value is 0");
    card.suit = Suit.Hearts;
    assert(card.suit != Suit.Spades, "cases compare with == and !=");

    # 'as' turns a case into its integer; the reverse is not allowed.
    assert((Suit.Hearts as int) == 2, "cases number from 0");
    assert((Status.Failed as int) == 45, "numbering continues after an explicit value");
    assert(sizeof<Status>() == 1, "Status is stored as a u8");
    return 0;
}

Declaring an enum

enum Name { ... } lists the cases, separated by commas; a comma after the last case is optional. Case names are PascalCase, like types (see naming conventions). A case may share a type’s name (String(value: String)); a string literal in the enum’s methods is still a String, and String(...) there calls the case.

Each case is an integer. The first case is 0 and each case after it is one more than the one before. A case can give its own value with = value, and numbering continues from there:

enum Status: u8 {
    Ok,             # 0
    NotFound = 44,  # 44
    Failed,         # 45
}

A value must be an integer constant: a literal such as -1 or 0x20, or a character such as 'y'. Two cases cannot have the same value.

The type after the colon is the representation, the integer type the enum is stored as. It can be any integer type (u8, short, int, long, and so on). Without one, the enum is stored as an int. Every case value must fit in the representation.

A case can carry a [Target] attribute, so a platform can have cases that others lack. A dropped case does not renumber the others.

Using enum values

This section is about enums whose cases carry nothing.

Name.Case is a value of the enum type. Such an enum is a value type, like int: it is copied, not reference counted, and it can be stored in locals, fields, arrays and module-scope constants, and passed to and returned from functions.

  • == and != compare two values of the same enum. Other operators apply only when the enum declares them (see Operators and indexers).
  • An enum does not mix with integers or with other enums: int x = Color.Red and Color c = 0 are errors, and so is comparing a Color with a Size.
  • value as int (or any integer type) gives a case’s integer value. The reverse, 1 as Color, is an error, since the integer may not name a case; name the case, or use from_value.
  • A slot of an enum type that is not given a value (a field, or a local declared without one) holds the case whose value is 0, or the first case when no case is 0.
  • sizeof<Name>() is the size of the representation.
  • An enum works wherever an integer does: as a HashMap key or HashSet element, in an Atomic<Name>, and in an extern function’s signature, where C sees the representation (an int unless the enum names another).

Cases that carry values

A case can carry values, declared like parameters: Circle(radius: int). An enum with such a case is a reference type: each value is an object, reference counted like a struct.

# Cases that carry values: each case is a constructor, and a `match`
# pattern binds the values of the case it matches.
enum Shape {
    Circle(radius: int),
    Rect(width: int, height: int),
    Square(side: int),
    Point,

    area(): int {
        match (this) {
            Circle(r) => { return 3 * r * r; }
            Rect(w, h) => { return w * h; }
            Square(s) => { return s * s; }
            Point => { return 0; }
        }
        return 0;
    }
}

# A generic enum can name itself: a list is empty, or a value and the rest.
enum List<T> {
    Empty,
    Cons(head: T, tail: List<T>),
}

length(list: List<String>): int {
    match (list) {
        Empty => { return 0; }
        # `_` skips a value.
        Cons(_, tail) => { return 1 + length(tail); }
    }
    return 0;
}

main(): int {
    var shapes = Array<Shape>();
    shapes.append(Shape.Circle(2));
    shapes.append(Shape.Rect(3, 4));
    shapes.append(Shape.Point);
    var total = 0;
    for (var s in shapes) {
        total += s.area();
    }
    assert(total == 24, "12 + 12 + 0");

    # Several patterns in one arm bind the same names.
    var side = 0;
    match (Shape.Square(5)) {
        Rect(w, _), Square(w) => { side = w; }
        else => {}
    }
    assert(side == 5, "a square's side");

    var names = List<String>.Cons("a", List<String>.Cons("b", List<String>.Empty));
    assert(length(names) == 2, "two names");
    assert(names.name() == "Cons", "name() spells the case");
    return 0;
}
  • Name.Case(values...) makes a new value, like a constructor call, and Name.Case alone is a case that carries nothing. An enum’s values are made only by its cases: Shape(...) is an error.
  • A case that carries values must be given them: Shape.Circle without (radius) is an error.
  • The values are fixed once made. There is no way to change them, so a value can be shared freely. To change one, make a new value.
  • Each case that carries nothing is a single object that every use shares. It is never freed and costs no allocation, however often it is used.
  • A slot of the enum type that is not given a value holds null, as a struct slot does. === and !== compare identity, and so does ==, unless the enum is [Equatable].
  • Mark the enum [Equatable] to derive == and hash from its cases: two values are equal when they hold the same case with equal values, so they work as HashMap and HashSet keys.
  • An enum can have type parameters (enum List<T>), and its cases can hold the enum itself, directly or through other types, so lists and trees are enums.
  • The enum can be [Shared] when every value its cases carry is thread-safe (see What threads can share). An enum whose cases carry nothing is stored as an integer and is always thread-safe. It can conform to a trait with extend Name with Trait, like a struct, and its methods can come from an extend block too.
  • Cases cannot have explicit values (= 3), and the enum has no representation type (: u8), since its values are not integers.

Value enums

An enum whose cases carry values can be marked [Value], like a value struct. Each value is then stored inline in whatever holds it, and copied when assigned, passed or returned, instead of being an object. Making one allocates nothing, which suits small enums that are made often, such as Optional and Result, which are both value enums.

[Value]
enum Shape {
    Circle(radius: int),
    Rect(width: int, height: int),
    Point,
}
  • Its cases share one storage: a value is as large as its largest case, plus the tag that says which case it holds (sizeof<Shape>() is 12: the tag, then room for Rect’s two ints, which Circle’s radius reuses).
  • A value enum is never null. A slot that is not given a value holds the first case, with every value it carries zero, false, or null: an unset Shape is Circle(0).
  • == compares the cases and their values; ===, !== and __null are errors.
  • Its cases cannot hold the enum itself, since the value would be infinitely large. Use a reference enum for lists and trees.
  • Everything else a value struct cannot do applies too, and so does boxing when it flows into a trait value.

Methods

An enum can declare methods after its cases. Inside a method, this is the value the method was called on, and a bare call of another method of the enum (next()) calls it on this. A static method is called on the enum itself (Light.first()). A method can be generic (choose<T>), with its type arguments inferred or written out (light.choose<String>(...)).

# An enum can declare methods after its cases; `this` is the value.
enum Light: u8 {
    Red,
    Amber,
    Green,

    may_go(): bool {
        return this == Light.Green;
    }

    next(): Light {
        match (this) {
            Red => { return Light.Green; }
            Amber => { return Light.Red; }
            Green => { return Light.Amber; }
        }
        return this;
    }

    # A bare call of another method acts on `this`.
    after_two(): Light {
        return next().next();
    }

    # A method can be generic, like any function.
    choose<T>(stop: T, go: T): T {
        return may_go() ? go : stop;
    }

    static first(): Light {
        return Light.Red;
    }
}

main(): int {
    var light = Light.first();
    assert(!light.may_go(), "red means stop");
    assert(light.next().may_go(), "green follows red");
    assert(light.after_two() == Light.Amber, "then amber");
    assert(light.choose("wait", "walk") == "wait" && Light.Green.choose(0, 1) == 1, "a generic method");

    # Every enum has name() and from_value().
    assert(light.name() == "Red", "name() spells the case");
    var found = Light.from_value(2);
    assert(found.is_ok() and found.value() == Light.Green, "2 is Green");
    assert(!Light.from_value(7).is_ok(), "no case has the value 7");

    # Enums key hash maps and fill atomics, like integers.
    var waits = HashMap<Light, int>();
    waits[Light.Red] = 30;
    assert(waits[Light.Red] == 30, "an enum is a hash key");
    return 0;
}

For an enum whose cases carry nothing, this is passed by value, like an int: a method cannot change the value it was called on. Return a new case instead, as next() does. Such an enum can also declare a trait with extend Light with Describe { ... }, which makes it meet the constraint T: Describe (see numbers, bool and enums); it is still never a trait value. For an enum whose cases carry values, this is the object, as in a struct’s method; its values still cannot change.

Operators and indexers

An enum can declare operators and an indexer the way a struct does: a method named after the operator (<(other: Level): bool) or []. For an enum whose cases carry nothing, a < b calls Level.< with a as this.

# An enum can declare operators and an indexer, as a struct does.
enum Level: u8 {
    Low,
    Mid,
    High,

    # `a < b` calls `Level.<`: the cases rank in order.
    <(other: Level): bool {
        return (this as int) < (other as int);
    }

    # `level + 1` moves up a level, stopping at the top.
    +(steps: int): Level {
        var next = Level.from_value((this as int + steps) as u8);
        return next.is_ok() ? next.value() : Level.High;
    }

    # `-level` mirrors it.
    -(): Level {
        return match (this) {
            Low => Level.High,
            Mid => Level.Mid,
            High => Level.Low,
        };
    }

    # `level[i]` reads, like any indexer.
    [](bit: int): bool {
        return ((this as int) >> bit) & 1 == 1;
    }
}

main(): int {
    var level = Level.Low;
    assert(level < Level.Mid && !(Level.High < level), "cases rank in order");
    assert(level + 1 == Level.Mid && level + 5 == Level.High, "a step up");
    assert(-level == Level.High, "mirrored");
    assert(Level.Mid[0] && !Level.Mid[1] && Level.High[1], "bits");
    return 0;
}

Its values never change and it already compares with == and !=, so such an enum cannot declare ==, !=, a compound assignment (+=), an indexer that assigns ([](index, value)) or the truth test !!, and its operators are not static.

Properties

An enum can also declare computed properties after its cases, var name: T { get { ... } }, which are read like fields (size.ounces). Inside the enum, a bare property name reads it on this.

# An enum can declare computed properties after its cases, read like
# fields: `size.ounces`, not `size.ounces()`.
enum Size {
    Small,
    Medium,
    Large,

    var ounces: int {
        get {
            return match (this) {
                Small => 8,
                Medium => 12,
                Large => 16,
            };
        }
    }

    # Inside the enum, a bare property name reads it on `this`.
    var cups: int {
        get { return ounces / 8; }
    }
}

# The same works for an enum whose cases carry values.
enum Shape {
    Circle(radius: int),
    Square(side: int),

    var area: int {
        get {
            return match (this) {
                Circle(r) => 3 * r * r,
                Square(s) => s * s,
            };
        }
    }
}

main(): int {
    var size = Size.Medium;
    assert(size.ounces == 12 && Size.Large.cups == 2, "value enum properties");
    assert(Shape.Square(4).area == 16 && Shape.Circle(2).area == 12, "properties that read the payload");
    return 0;
}

An enum value has nowhere to keep anything and never changes, so an enum’s property has a get with a body that does not use field, and no set:

main.pep:6:9: error: property 'n' of enum 'E' must be computed: give 'get' a body that does not use 'field'
main.pep:6:38: error: property 'n' of enum 'E' cannot have a 'set' accessor: enum values are immutable

Built-in methods

Every enum has these methods without declaring them, and cannot declare methods of the same names:

MethodWhat it does
value.name()Returns the case’s name as written ("Red"), a String. Every enum has it.
Name.from_value(n)Returns a Result<Name>: Ok with the case whose value is n, or an Err when no case has it. n has the enum’s representation type. Only enums whose cases carry nothing have it.

from_value is the checked way back from an integer (from a file, a C library or the network) to a case, since as only goes the other way.

match

match (value) { ... } runs the arm for the case that value holds:

# 'match' runs the arm of the case a value holds, and must handle every case.
enum Light {
    Red,
    Amber,
    Green,
}

next(light: Light): Light {
    match (light) {
        Red => { return Light.Green; }
        Amber => { return Light.Red; }
        Green => { return Light.Amber; }
    }
    return light;
}

may_go(light: Light): bool {
    match (light) {
        Green => { return true; }
        else => { return false; }
    }
    return false;
}

wait_seconds(light: Light): int {
    match (light) {
        Red, Amber => { return 30; }
        Light.Green => { return 0; }
    }
    return 0;
}

main(): int {
    var light = Light.Red;
    var changes = 0;
    while (!may_go(light)) {
        light = next(light);
        changes += 1;
    }
    assert(changes == 1, "red turns green");
    assert(wait_seconds(Light.Amber) == 30, "one arm handles two cases");
    assert(wait_seconds(light) == 0, "a pattern may name the enum");
    return 0;
}
  • Each arm is one or more patterns, separated by commas, then => and a block. A pattern is a case name (Red) or the case qualified by its enum (Light.Red).
  • A pattern for a case that carries values can bind them: Rect(w, h) declares w and h in its arm, holding the case’s values in order. The bindings are const. _ skips a value. A pattern lists all of its case’s values or, written without parentheses (Rect), none. Each value can also be a pattern of its own (see Nested patterns).
  • Patterns that share an arm must bind the same names, with the same types (Rect(w, _), Square(w)).
  • An else => { ... } arm, last, handles every case the other arms do not.
  • The arms must handle every case. A missing case is an error that names it, located at match; the language server offers a quick fix that adds an empty arm for each missing case. An else that can never run, because the other arms handle every case, is a warning.
  • Handling a case twice is an error.
  • In a match statement, break, continue and return inside an arm act on the enclosing loop or function, as they would inside an if.

The value is evaluated once, before any arm runs. A match on a null value of an enum whose cases carry values panics with match on a null 'Name'; a value enum is never null.

match expressions, is, and literal patterns

# `match` produces a value, `is` tests one case, and a `match` on an
# integer, character, `bool` or `String` compares with literals.
enum Shape {
    Circle(radius: int),
    Rect(width: int, height: int),
    Point,
}

enum List {
    Empty,
    Cons(head: int, tail: List),
}

area(s: Shape): int {
    return match (s) {
        Circle(r) => 3 * r * r,
        Rect(w, h) => w * h,
        Point => 0,
    };
}

keyword(word: String): int {
    return match (word) {
        "import" => 1,
        "export", "extern" => 2,
        else => 0,
    };
}

# The arms' values are numbers of different types (`0` is an `int`,
# `length` a `ulong`), so the `match` has their common type, `ulong`, and
# converts to the `int` this function returns.
length_of(word: String): int {
    return match (word) {
        "" => 0,
        else => word.length,
    };
}

main(): int {
    var total = area(Shape.Rect(2, 3)) + keyword("extern");

    # `is` binds only in the block of the `if` or `while` it is the
    # condition of.
    var s = Shape.Circle(1);
    if (s is Circle(r)) {
        total += r;
    }

    var list = List.Cons(1, List.Cons(2, List.Empty));
    while (list is Cons(head, tail)) {
        total += head;
        list = tail;
    }

    var point = Shape.Point is Point;
    if (point && !(s is Rect)) {
        total += 100;
    }

    # A `match` on an integer needs an `else`: literals cannot list every
    # value.
    var size = match (total) {
        0 => "none",
        1, 2, 3 => "few",
        else => "many",
    };
    assert(size == "many", "total is past three");
    assert(length_of("four") == 4 && length_of("") == 0, "lengths convert");
    assert(total == 6 + 2 + 1 + 3 + 100, "every step adds up");
    return 0;
}
  • A match that produces a value can go wherever a value can: after var x =, return, in an argument. Each arm is patterns => value, and arms are separated by commas (one after the last is allowed). It follows the rules above: a pattern binds names for its arm’s value, and the arms must handle every case unless there is an else. The value has the first arm’s type, and every other arm’s value must convert to it. When the arms are numbers of different types, the value has their common type, as C’s ? : does (int and long give long), and converts from it wherever it goes (0 or a ulong length, returned as an int). A closure in an arm’s value cannot use a name the arm binds; use a match statement there.
  • value is Pattern is a bool: whether value holds that case (d is Line, !(d is Text)). It takes any one pattern. When it is the whole condition of an if or while, its bindings are declared in that block only: if (d is Text(t)) { ... } reads t inside the braces, and while (list is Cons(head, tail)) { ... } examines list again on every iteration. Anywhere else, is cannot bind names. is is a keyword only between a value and a pattern, so it stays usable as a name (token.is(text)).
  • A trait value is matched by type rather than by case: Circle(c) tests for the struct Circle and binds the value as one. See Converting a trait value back.
  • Literal patterns: a match on an integer, a character, a bool or a String compares with literals (1, -1, 'a', true, "import"), using ==. Literals cannot list every value, so such a match needs an else arm, except on a bool that handles both true and false. Matching the same literal twice is an error. A match on an enum takes its case names, not literals, and a match on int takes literals, not names (except constants).

Nested patterns

The values in a case’s parentheses are patterns, as a tuple pattern’s elements are, so patterns nest to any depth:

# Patterns nest: the values a case carries can be matched by patterns of
# their own, as tuple elements can.
enum Doc {
    Text(text: String),
    Line,
    Pair(left: Doc, right: Doc),
}

describe(found: Optional<Doc>): String {
    return match (found) {
        Empty => "nothing",
        # A literal compares the value: only an empty `Text`.
        Value(Text("")) => "empty text",
        # A case of the value's enum, binding its own values.
        Value(Text(t)) => t,
        Value(Line) => "a line",
        # Deeper still, with `_` for a value that does not matter.
        Value(Pair(Line, _)) => "a pair that starts with a line",
        Value(Pair) => "a pair",
    };
}

main(): int {
    assert(describe(Optional<Doc>.Empty) == "nothing", "no document");
    assert(describe(Optional<Doc>.Value(Doc.Text(""))) == "empty text", "a literal value");
    assert(describe(Optional<Doc>.Value(Doc.Text("hi"))) == "hi", "a bound value");
    assert(
        describe(Optional<Doc>.Value(Doc.Pair(Doc.Line, Doc.Text("x")))) == "a pair that starts with a line",
        "two levels deep"
    );

    # A bare name binds the value, unless the value's enum has a case of
    # that name: here `Empty` is `Optional<int>`'s case.
    var nested = Optional<Optional<int>>.Value(Optional<int>.Empty);
    var depth = match (nested) {
        Empty => 0,
        Value(Empty) => 1,
        Value(Value(n)) => 2 + n,
    };
    assert(depth == 1, "the inner Optional is empty");

    # Ranges and guards work inside too, and so does `is`.
    var count = Optional<int>.Value(7);
    var size = match (count) {
        Value(0) => "none",
        Value(1..10) => "a few",
        Value(n) if n < 0 => "negative",
        Value(_) => "many",
        Empty => "unknown",
    };
    assert(size == "a few", "a range inside a case");
    var pair = Doc.Pair(Doc.Text("left"), Doc.Line);
    if (pair is Pair(Text(word), Line)) {
        assert(word == "left", "is binds nested names");
    }
    return 0;
}
  • Each value takes _, a name, a case of its enum (with patterns of its own values: Value(Pair(Line, _))), a literal, a range or constant, or a tuple pattern.
  • A bare name binds the value, unless the value’s enum has a case of that name, which it then matches (Value(Empty)), or it names a module-scope constant. A name repeated in one pattern (Pair(x, x)) must be a case.
  • A nested pattern handles part of its case: the arms must still handle every value. A case that only nested patterns handle is reported with the value they miss (match on 'Optional<Doc>' does not handle 'Value(Line)'), and a pattern whose values the earlier arms already match is an error.
  • Patterns that share an arm bind the same names, each at the same place: Pair(Text(t), Line), Pair(Text(t), Pair) binds t from either, but Pair(Text(t), _), Pair(_, Text(t)) is an error.
  • A nested test reads a value only once the case around it matched, so the tests stop at the first part that does not match.

Ranges and constants

A match on an integer or a character can compare with a range of values, and a match on an integer, a character or a bool with a module-scope constant:

# Range and constant patterns: a `match` on an integer or a character
# compares with ranges (`1..9`, `'a'..>'z'`) and module-scope constants.
const LIMIT = 100;

size(n: int): String {
    return match (n) {
        0 => "none",
        # `1..10` stops before 10; `10..>99` includes 99.
        1..10 => "a few",
        10..>99 => "many",
        LIMIT => "the limit",
        else => "out of range",
    };
}

kind(c: char): String {
    return match (c) {
        'a'..>'z', 'A'..>'Z' => "letter",
        '0'..>'9' => "digit",
        else => "other",
    };
}

# Ranges that cover every value of the type need no `else`.
half(b: u8): int {
    return match (b) {
        0..128 => 0,
        128..>255 => 1,
    };
}

main(): int {
    assert(size(0) == "none" && size(9) == "a few" && size(10) == "many", "ranges");
    assert(size(100) == "the limit" && size(-1) == "out of range", "a constant, then else");
    assert(kind('q') == "letter" && kind('7') == "digit" && kind('!') == "other", "character ranges");
    u8 high = 200;
    assert(half(3) == 0 && half(high) == 1, "every u8 is handled");

    # In a tuple pattern, a range or a constant compares its element.
    var pair = (LIMIT, true);
    var which = match (pair) {
        (0..LIMIT, _) => 1,
        (LIMIT, true) => 2,
        (_, _) => 3,
    };
    assert(which == 2, "the constant matches the first element");
    return 0;
}
  • start..end matches from start up to but not including end, and start..>end includes end, as ranges do elsewhere. Each bound is a literal (-1, 'z') or a constant (MIN..MAX). Ranges match integers and characters only, not bool or String.
  • A name that is a module-scope constant (const LIMIT = 100;, declared in the file or imported, or limits.MAX through a module) compares the value with it, like a literal. A local variable is not a pattern; compare with it in a guard instead (n if n == limit).
  • Inside a tuple pattern, a range or a constant compares its element ((0..LIMIT, _)), and any other bare name binds it, as before.
  • When every bound is a literal, the compiler knows which values a pattern matches: a pattern whose values the earlier arms already match is an error (2..>5 after 1..9), and a match whose ranges and literals cover every value of its type (0..128 and 128..>255 for a u8) needs no else. A pattern with a constant bound covers nothing for this check.
  • A match on an enum takes its cases, not ranges or constants, even a constant holding one of its cases.

Guards

An arm can add a guard after its patterns, if condition: the arm runs only when one of its patterns matches and the condition holds. Otherwise the next arm is tried, as if this one had not matched.

# A guard (`if condition`) after an arm's patterns: the arm runs only when a
# pattern matches and the guard holds; otherwise the next arm is tried.
enum Shape {
    Circle(radius: int),
    Rect(width: int, height: int),
    Point,
}

describe(s: Shape): String {
    return match (s) {
        # The guard reads the names its patterns bind.
        Circle(r) if r > 10 => "a large circle",
        Circle => "a circle",
        Rect(w, h) if w == h => "a square",
        Rect => "a rectangle",
        Point => "a point",
    };
}

# A guarded arm handles nothing for the check that every value is handled,
# since its guard may fail: the `bool` match below still needs `false`.
toll(heavy: bool, axles: int): int {
    match (heavy) {
        true if axles > 4 => {
            return 30;
        }
        true => {
            return 20;
        }
        false => {
            return 10;
        }
    }
}

main(): int {
    assert(describe(Shape.Circle(20)) == "a large circle", "the guard holds");
    assert(describe(Shape.Circle(2)) == "a circle", "the guard fails, the next arm runs");
    assert(describe(Shape.Rect(3, 3)) == "a square", "equal sides");
    assert(describe(Shape.Rect(3, 4)) == "a rectangle", "unequal sides");
    assert(toll(true, 6) + toll(true, 2) + toll(false, 2) == 60, "each arm runs once");

    # Guards work on tuples too.
    var pair = (5, true);
    var kind = match (pair) {
        (n, true) if n > 3 => 1,
        (_, true) => 2,
        (_, false) => 3,
    };
    assert(kind == 1, "the first arm's guard holds");
    return 0;
}
  • The guard reads the names the arm’s patterns bind (Circle(r) if r > 10). With several patterns (Word(s), Quoted(s) if s.length == 0), it applies to the arm, whichever pattern matched.
  • Guards work in match statements and match expressions, over enums, literals and tuples. is takes no guard: write x is P && condition, or test the condition inside the if.
  • A guarded arm handles nothing when the compiler checks that every case is handled, since its guard may fail: a match whose only arm for Circle has a guard does not handle Circle, and a later unguarded arm (or else) must. An else after guarded arms is not unreachable.
  • A guarded arm for a case an earlier unguarded arm already handles is an error, like any case handled twice. Two guarded arms may name the same case.
  • A closure in a guard cannot use a name the arm binds.

Current limits

  • An enum whose cases carry nothing cannot be generic, by design: its value is a plain integer, so a type parameter would have nothing to describe. An enum whose cases carry values can be (enum List<T>).
  • An enum whose cases carry values compares by identity with ==, as a struct does, unless it is a [Value] enum, is [Equatable], or declares == (with a matching hash<T> to be a key).
  • A constant is a pattern only when it is declared at module scope.
  • A match cannot examine floating-point values.
  • The methods of an enum whose cases carry nothing have no value form: c.name without the call is an error (“‘name’ is a method of ‘Color’: call it as ‘name()’”), where a struct’s p.norm is a bound method. Wrap the call in a closure instead, () => c.name().
  • An enum whose cases carry nothing is extended only with a trait (extend Color with Describe { ... }); declare its other methods inside it. It is never a trait value, since a trait value is a reference to an object, which such an enum’s value is not.