Enums
An enum is a type whose values come from a fixed list of cases. A case can be a plain name, like a C enum constant, or it can hold values of its own, called its payload. This chapter covers declaring enums, creating their values, testing which case a value is and converting enums to and from integers. Taking payloads apart is mostly the job of match, covered in Pattern matching.
Declaring an enum
An enum declaration lists its cases, one per line or separated by commas on one line:
Direction: enum{ north east south west} Suit: enum { clubs, diamonds, hearts, spades } main: fun{ val heading = Direction.east val trump = Suit.hearts printf("%d %d\n", i32(heading == Direction.east), i32(trump != Suit.spades))}1 1
A case is written Type.case. Two values of a plain enum compare with == and !=; they have no ordering, so < and the other comparisons are errors. Case names are camelCase by convention.
An enum needs at least one case, and each case name can appear only once:
Direction: enum{ northCase 'north' is declared more than once in 'Direction' south northCase 'north' is declared more than once in 'Direction'} Nothing: enum'Nothing' needs at least one case{} main: fun{ val a = Direction.north < Direction.southNo operation defined for '<' between 'Direction' and 'Direction'}Case values
Every case has an integer value. The first case is 0 and each case after it is one more than the case before. A case can set its own value with =, and the cases after it count on from there:
Status: enum{ ok notReady timeout lost = -1 busy = 100 retry} main: fun{ printf("%d %d %d %d %d %d\n", i32(Status.ok), i32(Status.notReady), i32(Status.timeout), i32(Status.lost), i32(Status.busy), i32(Status.retry))}0 1 2 -1 100 101
A value can be any constant integer expression. It can use constants and earlier cases of the same enum by their bare names, and cases of other enums once they're converted to an integer:
base: const i32 = 40 Level: enum{ none low = base / 8 high = low * 3 top = i32(Direction.west) + high} Direction: enum { north, east, south, west } main: fun{ printf("%d %d %d\n", i32(Level.low), i32(Level.high), i32(Level.top))}5 15 18
The value has to be known when the program is compiled, and it has to be an integer. Two enums whose values depend on each other are an error:
Dynamic: enum{ zero other = runtime()The value of 'other' must be a constant} Mixed: enum{ zero half = 1.5The value of 'half' must be an integer} First: enum { zero, a = i32(Second.b) }'First' depends on its own values Second: enum { zero, b = i32(First.a) }'Second' depends on its own values main: fun{}Two cases with the same value are two names for the same case. Here stop and halt are interchangeable:
Signal: enum{ go stop halt = 1} main: fun{ printf("%d\n", i32(Signal.stop == Signal.halt))}1
The tag type
An enum is stored as an integer, called its tag. Without a tag type, the compiler picks the smallest integer type that holds every case value. Writing an integer type in parentheses after enum sets the tag type yourself:
Small: enum { a, b, c } Signed: enum { zero, low = -50 } Wide: enum { zero, big = 70000 } Code: enum(u16){ none first = 100} main: fun{ printf("%d %d %d %d\n", i32(sizeOf(Small)), i32(sizeOf(Signed)), i32(sizeOf(Wide)), i32(sizeOf(Code)))}1 1 4 2
The tag type can be any integer type. With a tag type, every case value must fit in it, and a typed value must convert to it without loss:
Byte: enum(u8){ low high = 300'300' doesn't fit in 'u8'} Counted: enum(u8){ zero last = 255 past'past' is 256, which doesn't fit in 'u8'} Typed: enum(u8){ zero wide = 5u16Cannot convert type 'u16' to 'u8'} Fraction: enum(f32)'f32' isn't an integer type{ zero} main: fun{}The default case
The case with the value 0 is the enum's default. A variable declared without a value starts as that case, and so does an enum field of a record built with T():
Mode: enum{ active = 1 idle = 0 paused = 2} Settings: rec{ mode: Mode level: i32} main: fun{ var mode: Mode val settings = Settings() printf("%d %d\n", i32(mode == Mode.idle), i32(settings.mode == Mode.idle))}1 1
A case with the value 0 is required. Only an ext enum, covered in Calling C, and the tagless enums at the end of this chapter can do without one:
Rank: enum'Rank' needs a case with the value 0{ one = 1 two = 2} main: fun{}Payloads
A case can hold values. Its payload is written like a parameter list, with a name and a type for each value. A case with a payload is created by calling it with one argument per value:
Shape: enum{ empty circle(radius: f64) rect(width: f64, height: f64)} area: fun(shape: Shape) -> f64 => match shape{ empty => 0.0 circle(val r) => 3.0 * r * r rect(val w, val h) => w * h} main: fun{ val shapes = [Shape.circle(2.0), Shape.rect(3.0, 4.0), Shape.empty] for shape in shapes printf("%.1f\n", area(shape))}12.0 12.0 0.0
The match in area picks the arm for the case and binds the payload's values to r, w and h. Pattern matching covers this in full.
A case must be given exactly the values it holds, and a case without a payload takes no parentheses:
Shape: enum{ empty circle(radius: f64) rect(width: f64, height: f64)} main: fun{ val a = Shape.circle'Shape.circle' holds 1 value: radius val b = Shape.rect(1.0)'Shape.rect' holds 2 values: width, height val c = Shape.empty()'Shape.empty' holds no values val d = Shape.circle(true)Cannot convert type 'bool' to 'f64'}Each payload value needs a name, and names can't repeat within a case. A case can't hold its own enum directly, since the value would have to contain itself; it can hold a pointer to one instead:
Effect: enum{ none hit(i32)Expected ':' and a type} Heal: enum{ none heal(amount: i32, amount: i32)Payload 'amount' is declared more than once in 'heal'Payload 'amount' is declared more than once in 'heal'} List: enum{ end link(value: i32, next: List)Payload 'next' of type 'List' causes a cycle in the memory layout} main: fun{}Values of an enum with payloads have no ==; test them with is or match instead. Such an enum is stored as its tag followed by room for its largest payload:
Effect: enum{ none hit(amount: i32) scale(factor: f64)} main: fun{ printf("%d %d\n", i32(sizeOf(Effect)), i32(alignOf(Effect)))}16 8
If the case with the value 0 holds values, the enum has no default, and a variable of it must be given a value before it's read:
Effect: enum{ hit(amount: i32) none} main: fun{ var effect: Effect val copy = effectCannot use uninitialized values}Cases that share a value must also hold the same payload types:
Effect: enum{ none hit(amount: i32) = 1'hit' and 'miss' share the value 1 but have different payloads miss(chance: f32) = 1'hit' and 'miss' share the value 1 but have different payloads} main: fun{}Bare case names
Where the compiler already knows which enum it needs, a case can be written without the type name. That covers typed declarations, arguments, return values, assignments, array elements and comparisons with a value of the enum:
Team: enum { red, blue } Effect: enum{ none hit(amount: i32)} opposite: fun(team: Team) -> Team{ if team == red ret blue ret red} strength: fun(effect: Effect) -> i32 => match effect{ none => 0 hit(val n) => n} main: fun{ var team: Team = red team = opposite(team) val effects: array[Effect, 2] = [hit(5), none] printf("%d %d %d\n", i32(team), strength(effects[0]), strength(hit(7)))}1 5 7
Without an expected type, a bare case name is an error. A name that could also mean a variable or function of the expected type is ambiguous, and has to be written in full:
Team: enum { red, blue } blue: val Team = Team.red main: fun{ val team = redCannot infer the type of 'red' val same = Team.red == blue'blue' is ambiguous}Testing for a case
value is case is true when the value is that case. For a case with a payload, the payload goes in parentheses: _ accepts any value, and val name binds the value to a name that can be used where the test is known to be true:
Effect: enum{ none hit(amount: i32) heal(amount: i32)} main: fun{ val effect = Effect.hit(9) printf("%d %d %d\n", i32(effect is none), i32(effect is hit(_)), i32(effect is Effect.heal(_))) if effect is hit(val n) && n > 5 printf("big hit %d\n", n)}0 1 0 big hit 9
The case after is can be bare or qualified. is accepts every pattern match does; Pattern matching lists them.
Converting to and from integers
An enum without payloads converts to any integer type with T(x), which gives its case value. An integer converts back with the enum's name:
Code: enum{ none first = 100 second} main: fun{ val code = Code.second printf("%d %d\n", i32(code), u8(code)) val raw: u16 = 100 val back = Code(raw) printf("%d\n", i32(back == Code.first))}101 101 1
These conversions are never implicit. A constant that matches no case is a compile error, and a value that matches no case at run time panics:
Code: enum{ none first = 100} main: fun{ val bad = Code(50)'Code' has no case with the value 50} number: fun -> i32 => Code.firstCannot convert type 'Code' to 'i32'Code: enum{ none first = 100} main: fun{ val raw = 50 val _ = Code(raw)}Panic at main.ce:12:10: 'Code' has no case with this value
An enum with payloads has no conversion to or from integers. An enum can't declare its own conversions to or from integer types either, since those are built in (Conversions covers declared conversions):
Effect: enum{ none hit(amount: i32)} Color: enum{ red green as: op(self) -> i32 => 0Conversion to 'i32' is built in} main: fun{ val n = i32(Effect.none)No constructor for 'i32' accepts argument of type 'Effect'}Methods
An enum can declare methods, static functions, properties and static fields, as a record does; Methods and properties covers them. Inside a method, self is the enum value, so match self is the usual way to look at it, and a mut self method can replace self with another case:
Effect: enum{ none hit(amount: i32) heal(amount: i32) strong: fun(amount: i32) -> Effect => hit(amount * 2) size: fun(self) -> i32 => match self { none => 0 hit(val n) => n heal(val n) => n } isHarmful: get(self) -> bool => self is hit(_) double: fun(mut self) { self = match self { none => none hit(val n) => hit(n * 2) heal(val n) => heal(n * 2) } }} main: fun{ var effect = Effect.strong(3) effect.double() printf("%d %d\n", effect.size(), i32(effect.isHarmful))}12 1
An enum can't declare fields, constructors or a destructor, and a method can't share a name with a case:
Effect: enum{ none hit(amount: i32) count: i32Cannot declare fields in enums new: opCannot declare constructors in enums { }} Shot: enum{ miss hit(amount: i32)'hit' is declared more than once in 'Shot' hit: fun -> Shot => miss'hit' is declared more than once in 'Shot'} main: fun{}Owned enums
A plain enum copies like an integer. own enum declares an enum whose values can't be copied, only moved, the way own rec does for records. After a move, the old variable can't be used:
Ticket: own enum{ open(id: i32) closed} main: fun{ val ticket = Ticket.open(1) val moved = ticket val again = ticketCannot use moved valuesMoved on line 12}An enum whose payloads hold owning values, such as a record with a destructor, must be declared own enum ("Cannot store owning values in enums that aren't 'own'"). Ownership and destructors explains moves.
Generic enums
An enum can take type parameters, written after its name. Each use names the type, or lets the compiler infer it:
Maybe[T]: enum{ nothing just(value: T)} half: fun(n: i32) -> Maybe[i32]{ if n % 2 != 0 ret nothing ret just(n / 2)} main: fun{ val results = [half(8), half(3)] for result in results { match result { just(val v) => printf("%d\n", v) nothing => printf("odd\n") } }}4 odd
Generics covers type parameters and their constraints, including [T: enum](generics.md#enum) for generic code over enums without payloads.
Tagless enums
enum match T declares an enum that stores a value of type T and nothing else. Its cases sort the values of T into groups: a case lists the values it stands for after =, and one case written = else takes every other value. A case can have a payload written as a name with no type, which is the value itself, and the else case must have one:
Socket: enum match i32{ invalid = -1 open(handle) = else} Parity: enum match u8{ even = 0, 2, 4, 6 odd = 1, 3, 5, 7 large(value) = else} describe: fun(p: Parity) -> i32 => match p{ even => 0 odd => 1 large(val v) => i32(v)} main: fun{ val raw = 7 val socket = Socket(raw) match socket { open(val h) => printf("handle %d\n", h) invalid => printf("invalid\n") } printf("%d %d %d\n", describe(Parity(3)), describe(Parity(40)), i32(sizeOf(Socket))) printf("%d %d\n", i32(Socket.invalid), i32(socket))}handle 7 1 40 4 -1 7
A tagless enum takes no more room than T, and converting between it and T never changes the value. Converting from T can't fail, since the else case accepts everything the other cases don't. A case with a payload can be created with a value, which has to be one that case stands for; a constant that isn't is a compile error, and a value that isn't panics at run time:
Socket: enum match i32{ invalid = -1 open(handle) = else} main: fun{ val bad: Socket = open(-1)'Socket.open' can't hold -1}Socket: enum match i32{ invalid = -1 open(handle) = else} main: fun{ val raw = -1 val _: Socket = open(raw)}Panic at main.ce:12:18: 'Socket.open' can't hold this value
T can be an integer type, char, bool, a pointer type or a function type. For pointer and function types the only value a case can list is null, which makes an enum for pointers that may be null:
Node: rec{ value: i32} NodeRef: enum match ptr[Node]{ none = null some(node) = else} read: fun(n: NodeRef) -> i32 => match n{ some(val p) => p.value none => -1} main: fun{ var node = Node(5) val found: NodeRef = some(@node) val missing: NodeRef = none printf("%d %d %d\n", read(found), read(missing), i32(sizeOf(NodeRef)))}5 -1 8
A case without a payload needs a value, the values must be constants, and no value can be listed twice. The else case is needed unless the listed values cover every value of T, and there can be only one:
Repeats: enum match u8{ low = 1, 22 is listed more than once high = 2, 32 is listed more than once rest(value) = else} Missing: enum match u16'Missing' needs an 'else' case{ low = 1} Unvalued: enum match i32{ zeroCase 'zero' needs a value rest(value) = else} Floats: enum match f32Cannot declare 'enum match f32'{ any(value) = else} Pointers: enum match ptr[i32]{ zero = 0Only 'null' can be listed for 'ptr[i32]' other(value) = else} main: fun{}A tagless enum has no tag type, so enum(u8) match i32 is an error, and listing several values or writing else is only allowed in an enum match.
Cella Documentation