If we want to compare an expression to a value then we use an if
. But if we want to compare an expression to a lot of values this gets very tedious. Pony provides a powerful pattern matching facility, combining matching on values and types, without any special code required.
Here's a simple example of a match expression that produces a string.
match x
| 2 => "int"
| 2.0 => "float"
| "2" => "string"
else
"something else"
end
If you're used to functional languages this should be very familiar.
For those readers more familiar with the C and Java family of languages, think of this like a switch statement. But you can switch on values other than just integers, like Strings. In fact, you can switch on any type that provides a comparison function, including your own classes. And you can also switch on the runtime type of an expression.
A match starts with the keyword match
, followed by the expression to match, which is known as the match operand. In this example, the operand is just the variable x
, but it can be any expression.
Most of the match expression consists of a series of cases that we match against. Each case consists of a pipe symbol ('|'), the pattern to match against, an arrow ('=>') and the expression to evaluate if the case matches.
We go through the cases one by one until we find one that matches. (Actually, in practice the compiler is a lot more intelligent than that and uses a combination of sequential checks and jump tables to be as efficient as possible.)
Note that each match case has an expression to evaluate and these are all independent. There is no "fall through" between cases as there is in languages such as C.
If the value produced by the match expression isn't used then the cases can omit the arrow and expression to evaluate. This can be useful for excluding specific cases before a more general case.
As with all Pony control structures, the else case for a match expression is used if we have no other value, i.e. if none of our cases match. The else case, if there is one, must come at the end of the match, after all of the specific cases.
If the value the match expression results in is used then you need to have an else case, except in cases where the compiler recognizes that the match is exhaustive and that the else case can never actually be reached. If you omit it a default will be added which evaluates to None
.
The compiler recognizes a match as exhaustive when the union of the types for all patterns that match on type alone is a supertype of the matched expression type. In other words, when your cases cover all possible types for the matched expression, the compiler will not add an implicit else None
to your match statement.
The simplest match expression just matches on value.
fun f(x: U32): String =>
match x
| 1 => "one"
| 2 => "two"
| 3 => "three"
| 5 => "not four"
else
"something else"
end
For value matching the pattern is simply the value we want to match to, just like a C switch statement. The case with the same value as the operand wins and we use its expression.
The compiler calls the eq() function on the operand, passing the pattern as the argument. This means that you can use your own types as match operands and patterns, as long as you define an eq() function.
class Foo
var _x: U32
new create(x: U32) =>
_x = x
fun eq(that: Foo): Bool =>
_x == that._x
actor Main
fun f(x: Foo): String =>
match x
| Foo(1) => "one"
| Foo(2) => "two"
| Foo(3) => "three"
| Foo(5) => "not four"
else
"something else"
end
Matching on value is fine if the match operand and case patterns have all the same type. However, match can cope with multiple different types. Each case pattern is first checked to see if it is the same type as the runtime type of the operand. Only then will the values be compared.
fun f(x: (U32 | String | None)): String =>
match x
| None => "none"
| 2 => "two"
| 3 => "three"
| "5" => "not four"
else
"something else"
end
In many languages using runtime type information is very expensive and so it is generally avoided whenever possible.
In Pony it's cheap. Really cheap. Pony's "whole program" approach to compilation means the compiler can work out as much as possible at compile time. The runtime cost of each type check is generally a single pointer comparison. Plus of course, any checks which can be fully determined at compile time are. So for upcasts there's no runtime cost at all.
Sometimes you want to be able to match the type, for any value of that type. For this, you use a capture. This defines a local variable, valid only within the case, containing the value of the operand. If the operand is not of the specified type then the case doesn't match.
Captures look just like variable declarations within the pattern. Like normal variables, they can be declared as var or let. If you're not going to reassign them within the case expression it is good practice to use let.
fun f(x: (U32 | String | None)): String =>
match x
| None => "none"
| 2 => "two"
| 3 => "three"
| let u: U32 => "other integer"
| let s: String => s
else
"something else"
end
Can I omit the type from a capture, like I can from a local variable? Unfortunately no. Since we match on type and value the compiler has to know what type the pattern is, so it can't be inferred.
If you want to match on more than one operand at once then you can simply use a tuple. Cases will only match if all the tuple elements match.
fun f(x: (String | None), y: U32): String =>
match (x, y)
| (None, let y: U32) => "none"
| (let s: String, 2) => s + " two"
| (let s: String, 3) => s + " three"
| (let s: String, let u: U32) => s + " other integer"
else
"something else"
end
Do I have to specify all the elements in a tuple? No, you don't. Any tuple elements in a pattern can be marked as "don't care" by using an underscore ('_'). The first and fourth cases in our example don't actually care about the U32 element, so we can ignore it.
fun f(x: (String | None), y: U32): String =>
match (x, y)
| (None, _) => "none"
| (let s: String, 2) => s + " two"
| (let s: String, 3) => s + " three"
| (let s: String, _) => s + " other integer"
else
"something else"
end
In addition to matching on types and values, each case in a match can also have a guard condition. This is simply an expression, evaluated after type and value matching has occurred, that must give the value true for the case to match. If the guard is false then the case doesn't match and we move onto the next in the usual way.
Guards are introduced with the if
keyword (was where
until 0.2.1).
A guard expression may use any captured variables from that case, which allows for handling ranges and complex functions.
fun f(x: (String | None), y: U32): String =>
match (x, y)
| (None, _) => "none"
| (let s: String, 2) => s + " two"
| (let s: String, 3) => s + " three"
| (let s: String, let u: U32) if u > 14 => s + " other big integer"
| (let s: String, _) => s + " other small integer"
else
"something else"
end