Match Expressions

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.

Matching: the basics

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.

Else cases

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, even if it can never actually be reached. If you omit it a default will be added which evaluates to None. The compiler currently isn't clever enough to spot when the other cases are exhaustive and so the else is not needed. This will be changed later.

Matching on values

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 type and value

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 up casts there's no runtime cost at all.

Captures

Sometimes you want to be able to match on 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.

Matching tuples

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

Guards

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

results matching ""

    No results matching ""