Learn
Lesson 12

Enums

Say that a value is one of a fixed set of cases, carry data with each case, and let match check you've handled them all.

Some values can only be one of a few things: a traffic light is red, amber or green; a download is waiting, running, done or failed. An enum lists those cases, and the compiler makes sure every part of your program handles all of them.

main.osl
enum Light (
  Red
  Amber
  Green
)

Light now = Light.Red
log now == Light.Red
log now == Light.Green
Click the code to edit it

Write each case on its own line. Refer to a case through the enum's name: Light.Red.

match checks every case#

match on an enum must cover every case, so you can't forget one:

main.osl
enum Light (
  Red
  Amber
  Green
)

def next(Light l) Light -> match l (
  Light.Red -> Light.Green
  Light.Green -> Light.Amber
  Light.Amber -> Light.Red
)

def say(Light l) string -> match l (
  Light.Red -> "stop"
  Light.Amber -> "wait"
  Light.Green -> "go"
)

Light l = Light.Red
loop 4 (
  log say(l)
  l = next(l)
)
Click the code to edit it

Add a fourth case to Light and both functions stop compiling until you handle it:

main.osl
enum Light (
  Red
  Amber
  Green
  FlashingAmber
)

def say(Light l) string -> match l (
  Light.Red -> "stop"
  Light.Amber -> "wait"
  Light.Green -> "go"
)
Click the code to edit it

That's the point of an enum: when the set of cases changes, the compiler finds every place that needs updating.

Cases that carry data#

A case can hold a value, written in brackets after its name. Each case can hold a different type, or nothing at all:

main.osl
enum Shape (
  Circle(number)
  Square(number)
  Dot
)

def area(Shape s) number -> match s (
  Shape.Circle(r) -> 3.14 * r * r
  Shape.Square(side) -> side * side
  Shape.Dot -> 0
)

Shape[] shapes = [Shape.Circle(2), Shape.Square(3), Shape.Dot]
for s in shapes (
  log area(s)
)
Click the code to edit it

In Shape.Circle(r) -> ..., the name r takes the value the circle was made with, so the arm can use it.

A real example: loading#

Enums with data describe states well. A value can't be "loaded" and "failed" at once, and the data that goes with each state only exists in that state:

main.osl
enum Load (
  Waiting
  Done(string)
  Failed(string)
)

def show(Load state) string -> match state (
  Load.Waiting -> "loading…"
  Load.Done(text) -> "got " ++ text.len ++ " characters"
  Load.Failed(reason) -> "failed: " ++ reason
)

log show(Load.Waiting)
log show(Load.Done("hello world"))
log show(Load.Failed("timed out"))
Click the code to edit it

switch on an enum#

switch takes enum cases too, when each case should run code rather than produce a value. Inside the case, the cases are written without the enum name:

main.osl
enum Command (
  Move(int)
  Turn(string)
  Stop
)

Command[] program = [Command.Move(3), Command.Turn("left"), Command.Move(1), Command.Stop]
for c in program (
  switch c (
    case Move(steps)
      log "move " ++ steps
    case Turn(direction)
      log "turn " ++ direction
    case Stop
      log "stop"
  )
)
Click the code to edit it
Quick check

Why is this better than storing the state as a string like "waiting" or "done"?

Your turn

Coin values

Finish cents so it returns each coin's value: a penny is 1, a nickel 5, a dime 10 and a quarter 25. Then the program prints the total of the purse.

main.osl
Ctrl Enter runs
Expected output
66
0 of 24 lessons done