Pattern Matching
Matching on Enums
Flix supports pattern matching on algebraic data types.
For example, if we have an algebraic data type that models shapes:
enum Shape {
case Circle(Int32)
case Square(Int32)
case Rectangle(Int32, Int32)
}
Then we can write a function to compute the area of a Shape
using pattern
matching:
def area(s: Shape): Int32 = match s {
case Shape.Circle(r) => 3 * (r * r)
case Shape.Square(w) => w * w
case Shape.Rectangle(h, w) => h * w
}
Matching on Records
The above also works for record types; however, the syntax is slightly different.
Let us rewrite the Shape
type from before, this time using records.
enum Shape {
case Circle({ radius = Int32 })
case Square({ width = Int32 })
case Rectangle({ height = Int32, width = Int32 })
}
def area(s: Shape): Int32 = match s {
case Shape.Circle({ radius }) => 3 * (radius * radius)
case Shape.Square({ width }) => width * width
case Shape.Rectangle({ height, width }) => height * width
}
In the example above, we implicitly require that each pattern has exactly the specified labels. No more, no less. However, in general, the syntax for record patterns is similar to their types. Thus, we can match on a record that has at least one specific label.
def f(r: { height = Int32 | a }): Int32 = match r {
case { height | _ } => height
// The extension has a wildcard pattern since it is unused
}
Note, however, that the pattern also implies a type, thus the following example will not work.
def badTypes(r: { height = Int32 | a }): Int32 = match r {
case { height } => height
}
Additionally, all cases must have the same type, so this will also not work:
match ??? {
case { height | _ } => height
case { height } => height
}
This may be a contrived example, but it demonstrates a common pitfall, which is easily fixed.
This is because the first case is a polymorphic record
with a defined height
-label, whereas the second case
matches on a closed record that only has the
height
-label defined.
Additionally, the { label }
pattern is actually
syntactic sugar for { label = pattern }
.
Thus, if you are dealing with multiple records,
then it may be necessary to use different patterns.
def shadowing(r1: { height = Int32 | a }, r2: { height = Int32 | b }): Int32 =
match (r1, r2) {
case ({ height | _ }, { height | _ }) => height + height
// This does not work because `height = height` is defined twice
}
However, renaming the variables makes the program type check.
def renaming(r1: { height = Int32 | a }, r2: { height = Int32 | b }): Int32 =
match (r1, r2) {
case ({ height = h1 | _ }, { height = h2 | _ }) => h1 + h2
}
To summarize, here are a few examples of record patterns:
{ }
- the empty record{ radius = r }
- a record containg only the labelradius
where the value is bound tor
in the scope{ radius }
- a record containing only the labelradius
(this is actually syntactic sugar for{ radius = radius }
){ radius | _ }
- a record containg at least the labelradius
{ radius | r }
- a record containg at least the labelradius
where the rest of the record is bound tor
Let Pattern Match
In addition to the pattern match
construct, a let-binding can be used to
destruct a value. For example:
let (x, y, z) = (1, 2, 3)
Binds the variables x
, y
, and z
to the values 1
, 2
, and 3
,
respectively.
Any exhaustive pattern may be used in a let-binding. For example:
let (x, Foo(y, z)) = (1, Foo(2, 3))
is legal provided that the Foo
constructor belongs to a type where it is the
only constructor.
The following let-bindings are illegal because they are not exhaustive:
let (1, 2, z) = ...
let Some(x) = ...
The Flix compiler will reject such non-exhaustive patterns.
Let-pattern-matches work well with records, as they allow you to destructure a record and only use the labels you are interested in:
let { height | _ } = r;
height + height
Match Lambdas
Pattern matches can also be used with lambda expressions. For example:
List.map(match (x, y) -> x + y, (1, 1) :: (2, 2) :: Nil)
is equivalent to:
List.map(w -> match w { case (x, y) => x + y }, (1, 1) :: (2, 2) :: Nil)
As for let-bindings, such pattern matches must be exhaustive.
Note the difference between the two lambda expressions:
let f = (x, y, z) -> x + y + z + 42i32
let g = match (x, y, z) -> x + y + z + 42i32
Here f
is a function that expects three Int32
arguments, whereas g
is a
function that expects one three tuple (Int32, Int32, Int32)
argument.