Unions and match¶
A union is a type whose value is exactly one of several alternatives.
match is how a value of one is taken apart, one arm per alternative, and
the checker requires every alternative to be covered.
A union lists alternatives¶
type Shape = union { Circle, Square } declares that a Shape is a
Circle or a Square. A match over it binds each member at its own
type, and the arm sees that type's fields:
type Circle { radius : Float }
type Square { side : Float }
type Shape = union { Circle, Square }
fn area(s: Shape) : Float = match s {
Circle c => 3.0f * c.radius * c.radius,
Square q => q.side * q.side,
}
shapes : List<Shape> = [Circle(radius = 1.0f), Square(side = 2.0f)]
shapes | map(area)
[3.0, 4.0]
The annotation on shapes holds the literal at the union. Without it, a
list of a Circle and a Square is a type error: the two meet nowhere on their
own, and the message says the elements have no common type. A union is
written with = and union, and that is the only way a type is one of
several; type Id = Int is not a declaration.
A Circle passes where a Shape is expected with no step, since it is a
member. Going the other way, from the union to a member, is a match, or
is(Circle) and of_type(Circle) over a collection, as
subtypes and narrowing describes.
A match covers every case¶
A match is checked for coverage. Leaving a member out is a type error, and
the message says how to complete it:
type Circle { radius : Float }
type Square { side : Float }
type Shape = union { Circle, Square }
fn area(s: Shape) : Float = match s {
Circle c => 3.0f * c.radius * c.radius,
}
area(Circle(radius = 1.0f))
check error[non-exhaustive-match]: this match does not cover every Shape: cover every member of the union with a type pattern, or add `_`
--> <expression>:5:29
|
5 | fn area(s: Shape) : Float = match s {
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
_ fits anything. A match over an Int or a String has more values than
arms can name, so it needs one; a match over an Option needs some and
none, and one over a Result needs ok and err. This is what makes a
case safe to consume: a forgotten alternative is a type error, not a
silent default.
Literal patterns and guards¶
A pattern may be a literal, a name that binds the value, or _. A guard,
given after the pattern, narrows an arm with a condition. The first arm
whose pattern fits and whose guard holds produces the result:
fn sign(n: Int) : String = match n {
0 => "zero",
k given k < 0 => "negative",
_ => "positive",
}
[sign(-5), sign(0), sign(3)]
["negative", "zero", "positive"]
A guarded arm does not count toward coverage, since its condition may
fail. Two guarded arms over an Int with no _ are a type error, even where
their conditions together cover every number: the checker reads the
patterns, not the arithmetic.
Type patterns over a tree¶
Over a Data value, whose leaf could hold any scalar, an arm written with a
type both tests and reads: Int n fits a leaf holding an integer and binds
it as an Int.
fn classify(x: Data) : String = match x {
Int n => fmt("int {}", n),
String s => fmt("text {}", s),
_ => "other",
}
[classify(1), classify("hi"), classify(true)]
["int 1", "text hi", "other"]
The same arm reads a tree as a declared record, Book b, when the tree has
the record's fields at their types, as chapter 11
describes. Data is itself a union, declared in the standard library as a
scalar leaf, a sequence of trees or a string-keyed map of trees, which is
why whatever handles a Data may rely on there being three cases.
Nested patterns¶
A pattern nests: some(1) fits a present value equal to one, some(_) any
present value.
match [1, 2, 3].first() {
some(1) => "starts with one",
some(_) => "starts otherwise",
none => "empty",
}
"starts with one"
The short form¶
given … otherwise is a two-arm match on a Bool, read as a sentence:
this value, given a condition, otherwise that one.
[17, 42] | map(age => "adult" given age >= 18 otherwise "minor")
["minor", "adult"]
Without otherwise the result is an Option: a value given a condition,
and nothing otherwise, is a value that may be absent. "adult" given age >= 18
over the same two ages is [none, "adult"], and otherwise is the same
step as unwrap_or on that Option.