Variants and Matching
A variant is a value that is one of several cases. Together with
match, variants replace many of the
if chains, null checks and string tags
you'd write in JavaScript.
Declaring a variant
List the cases, separated by |. Case names start with a
capital letter, and a case can carry values:
types
Shape = Circle(Float) | Rect(Float, Float)
Status = Draft | Published(String) | Archived
A Shape is either a circle with a radius, or a
rectangle with a width and height. Make one by calling the case like
a function: Circle(1.0) or Rect(2.0, 3.0).
Cases with no values, like Draft, are written on their
own.
Matching on a variant
match looks at a value and runs the arm whose pattern
fits:
area(shape: Shape) -> Float {
match shape {
Circle(r) -> 3.14 * r * r,
Rect(w, h) -> w * h,
}
}
Each arm is pattern -> result, and arms are
separated by commas. The pattern Circle(r) matches
circles and names the radius r for that arm. Like
if, match is an expression, so its value
is the value of the arm that ran.
Every case is covered
Say we add a Triangle case to Shape and
forget to update area. In JavaScript that's a bug you'd
find at runtime, if you're lucky. Polar finds it right away:
error[POLAR0601]: this `match` doesn't cover every case
--> exhaust.px:8:5
|
8 | match shape {
| ^^^^^^^^^^^ `Triangle(_, _)` is not covered
This is one of the most useful things a type checker can do. When you add a case, the compiler gives you a list of every place that needs to handle it.
Patterns
match works on any value, not only variants. These are
the patterns you can use:
| Pattern | Matches |
|---|---|
0, "ann", true |
Exactly that value |
_ |
Anything, and ignores it |
name |
Anything, and names it |
Published(date) |
A case, naming its values |
[] |
An empty list |
[a], [a, b] |
A list of exactly that length |
[x, ..rest] |
A list with at least one item, and the rest of it |
{ x: 0, y: 0 } |
A record whose fields match |
Patterns nest, so Some([x, ..rest]) matches an
optional, non-empty list. Here are a few in use:
count(n: Int) -> String {
match n {
0 -> "none",
1 -> "one",
_ -> "many",
}
}
sum(xs: List<Int>) -> Int {
match xs {
[] -> 0,
[x, ..rest] -> x + sum(rest),
}
}
first_two(xs: List<String>) -> String {
match xs {
[] -> "nothing",
[a] -> "just #{a}",
[a, b, ..rest] -> "#{a} and #{b}, plus #{List.length(rest)} more",
}
}
Arms are tried top to bottom, so put specific patterns before general ones.
Note: _ covers every
remaining case, which also silences the "not covered" error.
Prefer listing cases by name when you can, so the compiler can
still warn you when a new case appears.
All together
module Variants
uses
Std.List
types
Shape = Circle(Float) | Rect(Float, Float)
Status = Draft | Published(String) | Archived
functions
area(shape: Shape) -> Float {
match shape {
Circle(r) -> 3.14 * r * r,
Rect(w, h) -> w * h,
}
}
label(status: Status) -> String {
match status {
Draft -> "draft",
Published(date) -> "published on #{date}",
Archived -> "archived",
}
}
count(n: Int) -> String {
match n {
0 -> "none",
1 -> "one",
_ -> "many",
}
}
sum(xs: List<Int>) -> Int {
match xs {
[] -> 0,
[x, ..rest] -> x + sum(rest),
}
}
first_two(xs: List<String>) -> String {
match xs {
[] -> "nothing",
[a] -> "just #{a}",
[a, b, ..rest] -> "#{a} and #{b}, plus #{List.length(rest)} more",
}
}
main() {
Log.info("#{area(Circle(1.0))} #{area(Rect(2.0, 3.0))}")
Log.info(label(Published("May 4")))
Log.info("#{count(0)}, #{count(1)}, #{count(7)}")
Log.info("#{sum([1, 2, 3, 4])}")
Log.info(first_two(["a", "b", "c", "d"]))
Log.info(first_two(["solo"]))
}
exports
main
$ polar run variants.px
3.14 6
published on May 4
none, one, many
10
a and b, plus 2 more
just solo
The standard library uses variants for its most important types. Let's meet them next: lists, options and results.