Skip to content
Menu
Contents

Variants and patterns

Chapter 5 of 9

Values that are one of several choices, and taking any value apart with match.

Variants

A variant is a value that is one of several choices. Each choice has a name that starts with a capital letter. This is a traffic light:

Light = [Red | Yellow | Green]

Read | as "or". A Light is Red or Yellow or Green.

A name that starts with a capital letter, on the left of =, declares a type. So Light = [Red | Yellow | Green] gives the list of choices the short name Light. After that you just write Red where you need it.

You often do not need that line. Bert learns the choices from a match that lists them. You need it when you want to write Light in a signature, or when a choice with no data is only ever written on its own, never matched. Then Bert cannot tell whether Red should carry data, and says so.

The compiler calls a choice a tag. You will see that word in its messages.

A choice can carry data. A shape is a circle with a radius, or a rectangle with a width and a height:

Circle(2)
Rect(3, 4)

Taking a variant apart with match

match looks at a variant and picks one of several cases. Each case is called an arm: a pattern, an arrow, and a result. match runs the arm that fits:

area = (shape) -> match shape {
  Circle(r) -> 3 * r * r
  Rect(w, h) -> w * h
}

area(Rect(3, 4))    # => 12
area(Circle(2))     # => 12

In Circle(r), the r is a new name for the value inside. You can use it in that arm.

A choice with no data is written plainly, with no parentheses: Red, not Red().

canGo : (Light) -> Bool
canGo = (light) -> match light {
  Red -> false
  Yellow -> false
  Green -> true
}

canGo(Green)    # => true

Here is the best part. The compiler checks that you covered every choice. Leave out the Yellow arm, and Bert reports an error and will not run the program. So you cannot forget a case.

match is not only for variants. It also works on whole numbers, strings and lists (more below), and on records, which come in the next chapter. Not on Float values or functions. To check a Float, compare it with <, > or == inside an if instead.

welcome = (name) -> match name {
  "" -> "anonymous"
  "admin" -> "welcome boss"
  _ -> name
}

The underscore _ means "anything else". Numbers and strings have endless values, so they always need one.

Pattern matching

You have seen match on variants, and a few times on other values. This section puts it all in one place, because match is how you take almost any value apart in Bert.

Every arm has a pattern on the left of ->. If the value fits the pattern, that arm runs. Arms are tried from the top, and the first one that fits wins.

The shapes of a pattern

PatternFits
0, true, "admin"exactly that value
nanything, and names it n
_anything, and ignores it
Ok(n), Rect(w, h)a variant, and names what is inside
[], [a, b], [a, ..rest]a list of a certain shape

List patterns

Lists have two shapes: empty, or a first item followed by the rest. Patterns follow that:

describe = (nums) -> match nums {
  [] -> "empty"
  [one] -> String.concat("just ", Int.toString(one))
  [a, b] -> String.concat("pair ", Int.toString(a + b))
  [first, .._] -> String.concat("starts with ", Int.toString(first))
}

describe([])           # => "empty"
describe([7])          # => "just 7"
describe([3, 4])       # => "pair 7"
describe([9, 8, 7])    # => "starts with 9"

Two details:

  • [a, b] fits a list of exactly two items.
  • ..rest fits whatever is left, and it can only go last. .._ means "the rest, but I do not need it".

Looping over a list

Put list patterns together with a function that calls itself, and you can walk a whole list:

sum = (nums) -> match nums {
  [] -> 0
  [first, ..rest] -> first + sum(rest)
}

sum([1, 2, 3])    # => 6

Read it like a definition. The sum of an empty list is 0. The sum of anything else is the first item plus the sum of the rest. Most of the time List.map and List.foldl are easier, but this is what they do underneath.

Note

In Functions you saw why first + sum(rest) is not a last step. So a very long list uses a lot of space here. For huge lists, use List.foldl.

Patterns nest

A pattern can sit inside another pattern, as deep as you like:

check = (result) -> match result {
  Ok(0) -> "zero"
  Ok(n) -> Int.toString(n)
  Err(_) -> "failed"
}

firstOk = (results) -> match results {
  [Ok(n), .._] -> n
  _ -> 0
}

Ok(0) is a variant holding a specific number. [Ok(n), .._] is a list whose first item is an Ok.

The compiler checks you

A match has to cover every possible value. If you leave a case out, the program does not build. If an arm can never be reached because an arm above it already catches everything, that is an error too.

For lists, [] plus [first, ..rest] covers everything. For whole numbers and strings there are too many values to list, so end with a _ arm.