Docs / Language Manual / PatternMatching

Pattern Matching

Make sure you've read up on Variant first.

We're finally here! Pattern matching is one of the best features of the language. It's like destructuring, but comes with even more help from the type system.

Usage

Consider a variant:

RE
type payload = | BadResult(int) | GoodResult(string) | NoResult;

While using the switch expression on it, you can "destructure" it:

RE
let data = GoodResult("Product shipped!"); let message = switch (data) { | GoodResult(theMessage) => "Success! " ++ theMessage | BadResult(errorCode) => "Something's wrong. The error code is: " ++ string_of_int(errorCode) };

Notice how we've destructured data while handling each different case. The above switch will give you a compiler warning:

Warning 8: this pattern-matching is not exhaustive. Here is an example of a value that is not matched: NoResult

Isn't that great? While matching on the shape of your data, the type system warned of an unhandled case. This conditional aspect is what makes it pattern matching rather than plain destructuring. Most data structures with an "if this then that" aspect work with it:

RE
switch (myList) { | [] => print_endline("Empty list") | [a, ...theRest] => print_endline("list with the head value " ++ a) }; switch (myArray) { | [|1, 2|] => print_endline("This is an array with item 1 and 2") | [||] => print_endline("This array has no element") | _ => print_endline("This is an array") };

The _ case is a special fall-through case that allows all unmatched conditions to go to that branch.

You can even switch on string, int and others. You can even have many patterns going to the same result!

RE
let reply = switch (message) { | "Reason's pretty cool" => "Yep" | "good night" => "See ya!" | "hello" | "hi" | "heya" | "hey" => "hello to you too!" | _ => "Nice to meet you!" };

Combined with other data structures, pattern matching can produce extremely concise, compiler-verified, performant code:

RE
let message = switch (data) { | GoodResult(theMessage) => "Success! " ++ theMessage | BadResult(0 | 1 | 5) => "Something's wrong. It's a server side problem." | BadResult(errorCode) => "Unknown error occurred. Code: " ++ string_of_int(errorCode) | NoResult => "Things look fine" };

Note: you can only pass literals (i.e. concrete values) as a pattern, not let-binding names or other things. The following doesn't work as expected:

RE
let myMessage = "Hello"; switch (greeting) { | myMessage => print_endline("Hi to you") };

Instead, it'd assume you're matching on any string, and binding that to the name myMessage in that switch case, which is not what you wanted.

When clauses

When you really need to use arbitrary logic with an otherwise clean pattern match, you can slip in some when clauses, which are basically if sugar:

RE
let message = switch (data) { | GoodResult(theMessage) => ... | BadResult(errorCode) when isServerError(errorCode) => ... | BadResult(errorCode) => ... /* otherwise */ | NoResult => ... };

Match on Exceptions

If the function throws an exception (covered later), you can also match on that, in addition to the function's normally returned values.

RE
switch (List.find((i) => i === theItem, myItems)) { | item => print_endline(item) | exception Not_found => print_endline("No such item found!") };

Nested Patterns

Nested | work as intended:

RE
switch (student) { | {name: "Jane" | "Joe"} => ... | {name: "Bob", Job: Programmer({fullTime: Yes | Maybe})} => ... };

Patterns Everywhere

You can put a pattern anywhere you'd put a normal "variable declaration":

RE
type leftOrRight = | Left(int) | Right(int); let i = Left(1); /* magic! */ let Left(v) | Right(v) = i;

Tips & Tricks

Flatten your pattern-match whenever you can. This is a real bug remover. Example below.

Do not abuse the fall-through _ case too much. This prevents the compiler from telling you that you've forgotten to cover a case (exhaustiveness check), which would be especially helpful after a refactoring where you add a new case to a variant. Try only using _ against infinite possibilities, e.g. string, int, etc.

Here's a series of examples, from worst to best:

RE
let optionBoolToBool = opt => if (opt == None) { false; } else if (opt == Some(true)) { true; } else { false; };

Now that's just silly =). Let's turn it into pattern-matching:

RE
let optionBoolToBool = opt => switch (opt) { | None => false | Some(a) => a ? true : false };

Slightly better, but still nested. Pattern-matching allows you to do this:

RE
let optionBoolToBool = opt => switch (opt) { | None => false | Some(true) => true | Some(false) => false };

Much more linear-looking! Now, you might be tempted to do this:

RE
let optionBoolToBool = opt => switch (opt) { | Some(true) => true | _ => false };

Which is much more concise, but kills the exhaustiveness check mentioned above; refrain from using that. This is the best:

RE
let optionBoolToBool = opt => switch (opt) { | Some(trueOrFalse) => trueOrFalse | None => false };

Pretty darn hard to make a mistake in this code at this point! Whenever you'd like to use an if-else with many branches, prefer pattern matching instead. It's more concise and performant too.

See another example, with switch + tuple here.

Design Decisions

The notorious fizzbuzz problem strangely trips up some people, partially due its nature of paralyzing the programmer who hopes to simplify/unify the few condition branches in search of elegance where there's none. Hopefully you can see that usually, pattern-matching's visual conciseness allows us to overcome decision paralysis, while keeping all the benefits (and more, as you've seen) of a bunch of brute-forced if-elses. There's really nothing wrong with explicitly listing out all the possibilities; Pattern matching corresponds to case analysis in math, a valid problem-solving technique that proves to be extremely convenient.

Using a Reason switch for the first time might make you feel like you've been missing out all these years. Careful, for it might ruin other languages for you =).

If you've tried to refactor a big, nested if-else logic, you might realize it's very hard to get the logic right. On the other hand, pattern matching + tuple conceptually maps to a 2D table, where each cell can be independently filled. This ensures that whenever you need to add a case in the switch, you can target that and only that table cell, without messing other cells up.

RE
type animal = Dog | Cat | Bird; let result = switch (isBig, myAnimal) { | (true, Dog) => 1 | (true, Cat) => 2 | (true, Bird) => 3 | (false, Dog | Cat) => 4 | (false, Bird) => 5 };
isBig \ myAnimalDogCatBird
true123
false445