Enums: one of several kinds
On this page
Some things come in kinds. A drink is an espresso, a latte or a tea. An order either goes through or fails, for one of a few reasons. An enum names every kind a value may be, and each value is exactly one of them. This page shows how to declare one, build its values, and find out which kind you have with match, the same match that chose among numbers in Conditionals. A small café takes orders by code number:
mod cafe;
import stdio use *;
// A plain enum: a closed set of names
enum Size {
Small, Medium, Large;
// One function over the whole enum, taking the value
pub fn ml(self Size) i32 {
match self {
case Small {240;}
case Medium {350;}
case Large {470;}
}
}
}
// Variants that carry values of their own, and a field they all share
enum Drink {
pub size Size;
Espresso;
struct Latte {
pub shots i32;
pub oat bool;
}
struct Tea {
pub minutes i32;
}
}
// Why an order can fail
enum Fault {
SoldOut;
NoSuchCode(i32);
}
// A price in cents. Every variant has a case, so no 'else' is needed.
fn price(d Drink) i32 {
let base = match d {
case Espresso {250;}
case let l Latte if l.oat {350 + 60 * l.shots;}
case let l Latte {320 + 60 * l.shots;}
case let t Tea {200 + 10 * t.minutes;}
};
if d.size == Size.Large {base + 80;} else {base;}
}
// A drink for a code on the menu, or the reason there is none
fn order(code i32) Result[Drink, Fault] {
match code {
case 1 {Ok(Drink.Espresso(Size.Small));}
case 2 {Ok(Drink.Latte(Size.Large, 2, true));}
case 3 {Ok(Drink.Tea(Size.Medium, 4));}
case 4 {Error(Fault.SoldOut);}
else {Error(Fault.NoSuchCode(code));}
}
}
fn main() {
each code in 1 .. 5 {
print <- "#", code, ": ";
match order(code) { // a Result is an enum too
case let d Ok {
print <- d.size.ml(), " ml for ", price(d), " cents";
if d is Tea {print <- " (let it steep)";}
print <- "\n";
}
case let f Error {
match f {
case SoldOut {print <- "sold out, sorry\n";}
case let n NoSuchCode {print <- "there is no drink ", n, "\n";}
}
}
}
}
}
It prints:
#1: 240 ml for 250 cents #2: 470 ml for 550 cents #3: 350 ml for 240 cents (let it steep) #4: sold out, sorry #5: there is no drink 5
Try this: add a hot chocolate to Drink, and watch the compiler point you to the one match that has to learn its price.
One of several kinds#
An enum declares a closed set of variants. The whole set is written in one place, inside the enum's braces, so the compiler knows every kind a value can be, and nothing elsewhere can add another. That is what lets it check a match for you. Leave the tea out of price and the program does not compile:
Error 1417: This match does not cover Tea: add a case for each, or end the match with 'else {}'.
So when the menu gains a hot chocolate next month, the compiler walks you to every match that names each variant and now has one more to learn. A match that ends in else has said what to do with anything it did not name, and is left alone.
Every value of an enum takes the same room in memory: the size of its largest variant, plus a small hidden tag that says which variant it is. That is why a Drink can be held in a variable, passed to a function and handed back as easily as a number, whichever kind of drink it is. It is copied like a number, too, unless one of its variants holds something that moves, such as a String: then the whole enum moves, since any value of it might be that variant.
Three ways to write a variant#
One enum may mix all three:
- A bare name, Espresso or Small, carries nothing of its own.
- A struct, struct Latte {...}, carries named fields, and may have methods, as any struct does.
- A name and a type in parentheses, NoSuchCode(i32), carries one value of that type, with no field name.
The three are less different than they look. A bare name is a struct with no fields: Espresso; declares just what struct Espresso; would. So a plain set of names, like Size, and a set of structs, like Drink, are the same construct, and there is no second one to learn.
A field written outside the variants, as size is in Drink, belongs to every variant, so it can be read from any Drink without asking which one it is: d.size. A variant's own fields wait until the program knows which variant it has: d.shots is refused, since only a latte has shots. A field every variant shares is told from a variant written with a type by its parentheses: size Size; is a field, NoSuchCode(i32); a variant. An enum with shared fields names everything it holds, so its variants are written as structs or bare names, never with a type in parentheses.
A value is built by naming its variant through the enum, with its values in parentheses, the shared ones first: Drink.Latte(Size.Large, 2, true). There is no new, and a bare name is a value all by itself: Size.Large. Each variant's value is of its own kind, a Drink.Latte, and it is taken wherever a Drink is wanted: by a parameter, a field, or a function's result, as order hands its variants straight to Ok and Error.
A var variable given a variant is the enum, since it is there to change: var light = Light.Red; followed by light = Light.Green; is fine, and so is var o = Some(5); o = None;. A let variable keeps the variant's own type, so its values stay in reach: let d = Drink.Latte(Size.Large, 2, true); reads d's Latte fields directly. A var variable meant to stay one variant says so: var d Drink.Latte = ... (the full rule). Comparing with == against a variant that holds no values tests which variant it is: light == Light.Green.
match on variants#
In Conditionals, match chose among numbers. Here it chooses among variants, and it is the same match: the cases are tried from the top, the first that fits runs, and the value of the block that runs is the value of the match, as let base = match d {...} shows. What is new is the pattern, which is a variant's name:
- case Espresso asks only whether the value is that variant (case is Espresso says the same).
- case let l Latte asks the same, and when it is, gives you the latte as l, with its own fields to read. The binding may be var, to change it.
- A guard narrows a case further, just as it does on a number. The oat-milk latte comes first; a latte without oat milk fails its guard and falls to the next case.
- For a variant written with a type in parentheses, the name you bind is what it holds: case let n NoSuchCode makes n the number itself.
Inside a pattern the variants are named bare. The match knows which enum it is looking at, so Latte means Drink.Latte without being told, and it asks that enum first, whatever else the name might mean nearby. A binding is the variant's value, taken from the value matched: a copy where it copies, and a move where it holds something that moves. Through a borrowed reference it is a reference into the variant, which Borrowed references explains.
When only one variant matters, a whole match is more than the question needs. is asks it, true or false, as d is Tea does in the program. And where the test holds, the compiler knows which variant the value is, so its own fields can be read:
fn shots(d Drink) i32 {
if d is Latte {
return d.shots; // here d is known to be a Latte
}
0;
}
A binding in an if tests and hands over the variant in one step, with an else for when it is something else:
if let l Latte = d {
print <- l.shots, " shots\n";
} else {
print <- "no shots\n";
}
And when the rest of a function only makes sense for one variant, the binding can be a declaration whose else leaves. Past it, the variable is there for the rest of the block. The else must not fall through: it ends in return, break, continue, or a call that never returns, such as panic:
fn steep(d Drink) i32 {
let t Tea = d else { return 0; };
t.minutes; // steep(Drink.Tea(Size.Large, 5)) is 5
}
Comparing enum values#
An enum whose variants are all bare names compares with == and !=, as d.size == Size.Large does: such a value is nothing but its tag. Comparing with a variant that carries nothing always works that way, whatever the enum's other variants carry: unit != Shape.Dot just asks whether unit is a Dot, and maybe == None whether an Option is empty. The other side may be the enum or any of its variants, so a variable that holds a variant compares too, with a variant written out or with another such variable: let s = Size.Large; s == Size.Large. In an enum with shared fields, though, every variant carries those, the bare names included.
Where both values may carry fields, two values are equal when they are the same variant and that variant's own == says so. The tags are compared first, and only if they match is the variant's == called, so each variant that carries fields declares one, as a struct would (Method patterns). Without one, comparing the enum is refused, and the error names the variant that lacks it; there is no comparison of a struct's contents to fall back on, and deriving one is still to come. A match recovers the variant instead. That includes Option and Result, whose Some, Ok and Error declare no ==: two Options holding values are matched, not compared, for now.
enum Shape {
struct Circle {
pub radius f32;
pub fn `==`(self, other Circle) bool {radius == other.radius;}
}
struct Dot; // equal to any other Dot
}
let unit Shape = Shape.Circle(1.0);
unit == Shape.Circle(1.0) // true: the same variant, equal fields
unit == Shape.Circle(2.0) // false
unit != Shape.Dot // true
Enums compare for equality only, never for order: which variant a value is, is not a size, and the order variants are declared in is not a magnitude. Comparing reads both values and changes neither, so even an enum that moves can be compared and used afterwards.
Methods on an enum#
pub fn ml(self Size) i32 names the enum as what it takes, so it is one function over the whole enum: inside it self is a Size, and match self asks which. A state that moves on is written the same way, as a function that takes a value and gives back the next:
enum Light {
Red, Green, Amber;
pub fn next(self Light) Light {
match self {
case Red {Light.Green;}
case Green {Light.Amber;}
case Amber {Light.Red;}
}
}
}
var light Light = Light.Red;
light = light.next(); // Green
light = light.next(); // Amber
Nothing is changed in place: next hands back a new value, and the variable takes it. A method written inside a variant's struct belongs to that variant alone, as a struct's method does. A method written among the variants that every variant should answer, each in its own way, is a step toward traits, which Traits covers along with the enum's part in them.
Naming variants: use#
A variant is a name of its enum, not of the module around it, so outside the enum it is reached through the enum: Size.Large, Fault.SoldOut. Two enums may each have a Quit without getting in each other's way. Inside the enum's braces, every name it declares is bare, its variants included, which is why ml can say Small.
A module that wants an enum's variants as names of its own says so with use, written at the module's top level, like an import:
use Size; // Small, Medium and Large, bare use Event KeyEvent as Key, QuitEvent; // only these, one of them renamed use Colors but Blue; // every variant but one
After use Size;, Large means Size.Large, in the module that declares the enum as in any other. A name folded in this way must not be one the module already has for something else, so two enums that each have a Quit are folded by renaming one or leaving it out. A pattern needs none of this, since it already knows its enum. Namespaces has the forms in full, and how pub use makes the names public.
Tag values#
Each variant's tag is a number the compiler assigns, counting up from 0 in the order the variants are written. A variant may give its own instead, and counting carries on from it, so only the variants that need a particular number say so. An enum may also name the integer type its tags are laid out in, which fixes their size. That is how an enum lines up with the constants of a library written in another language:
enum Colors u32 {
Red = 0xFF0000,
Green = 0x00FF00,
Blue = 0x0000FF;
}
The tag itself is never a field a program can read or change: which variant a value is, is found by matching, or compared with ==. Talking to C has the rest: negative values, how wide a tag is when no type is named, and placing the tag somewhere other than first.
Result and Option: the enums every program has#
Functions introduced Result: a function that can fail gives back Ok with its value or Error with what went wrong. Now it can be said what a Result is. The core library declares it as an ordinary enum, with two variants written as types:
enum Result[T, E] {Ok(T); Error(E);}
The [T, E] leave the two types open until a Result is used: a Result[Drink, Fault] holds a Drink when it is Ok and a Fault when it is an Error. Nothing else about it is special. match order(code) is the match you would write for any enum, and the error can be anything, an enum of your own included: Fault lists the ways an order can fail, and the inner match tells them apart.
Its partner is Option, for a value that may not be there at all:
enum Option[T] {None; Some(T);}
A lookup that may find nothing gives back an Option: Some with what it found, or None. The variants of both are bare in every program, because the core library makes them so. Option is taught where it is first needed, in Dict, at the first lookup that may come back empty.
You can now say that a value is one of several kinds, give each kind its own data, and handle every kind with the compiler checking that none is missed. Next come collections, which hold as many values as a program needs, and which hand you an Option when they have nothing to give.