F Sharp Programming/Pattern Matching Basics

From Wikibooks, open books for an open world
Jump to: navigation, search
Previous: Values and Functions Index Next: Recursion
F# : Pattern Matching Basics


Pattern matching is used for control flow; it allows programmers to look at a value, test it against a series of conditions, and perform certain computations depending on whether that condition is met. While pattern matching is conceptually similar to a series of if ... then statements in other languages, F#'s pattern matching is much more flexible and powerful.

Pattern Matching Syntax[edit]

In high level terms, pattern matching resembles this:

match expr with
| pat1 -> result1
| pat2 -> result2
| pat3 when expr2 -> result3
| _ -> defaultResult

Each | defines a condition, the -> means "if the condition is true, return this value...". The _ is the default pattern, meaning that it matches anything, sort of like a wildcard.

Using a real example, it's easy to calculate the nth Fibonacci number using pattern matching syntax:

let rec fib n =
    match n with
    | 0 -> 0
    | 1 -> 1
    | _ -> fib (n - 1) + fib (n - 2)

We can experiment with this function in fsi:

 fib 1;;
val it : int = 1
> fib 2;;
val it : int = 1
> fib 5;;
val it : int = 5
> fib 10;;
val it : int = 55

It's possible to chain together multiple conditions which return the same value. For example:

> let greeting name =
    match name with
    | "Steve" | "Kristina" | "Matt" -> "Hello!"
    | "Carlos" | "Maria" -> "Hola!"
    | "Worf" -> "nuqneH!"
    | "Pierre" | "Monique" -> "Bonjour!"
    | _ -> "DOES NOT COMPUTE!";;
 
val greeting : string -> string
 
> greeting "Monique";;
val it : string = "Bonjour!"
> greeting "Pierre";;
val it : string = "Bonjour!"
> greeting "Kristina";;
val it : string = "Hello!"
> greeting "Sakura";;
val it : string = "DOES NOT COMPUTE!"

Alternative Pattern Matching Syntax[edit]

Pattern matching is such a fundamental feature that F# has a shorthand syntax for writing pattern matching functions using the function keyword:

let something = function
    | test1 -> value1
    | test2 -> value2
    | test3 -> value3

It may not be obvious, but a function defined in this way actually takes a single input. Here's a trivial example of the alternative syntax:

let getPrice = function
    | "banana" -> 0.79
    | "watermelon" -> 3.49
    | "tofu" -> 1.09
    | _ -> nan (* nan is a special value meaning "not a number" *)

Although it doesn't appear as if the function takes any parameters, it actually has the type string -> float, so it takes a single string parameter and returns a float. You call this function in exactly the same way that you'd call any other function:

> getPrice "tofu";;
val it : float = 1.09
 
> getPrice "banana";;
val it : float = 0.79
 
> getPrice "apple";;
val it : float = nan

Comparison To Other Languages[edit]

F#'s pattern matching syntax is subtly different from "switch statement" structures in imperative languages, because each case in a pattern has a return value. For example, the fib function is equivalent to the following C#:

int fib(int n)
{
    switch(n)
    {
        case 0: return 0;
        case 1: return 1;
        default: return fib(n - 1) + fib(n - 2);
    }
}

Like all functions, pattern matches can only have one return type.

Binding Variables with Pattern Matching[edit]

Pattern matching is not a fancy syntax for a switch structure found in other languages, because it does not necessarily match against values, it matches against the shape of data.

F# can automatically bind values to identifiers if they match certain patterns. This can be especially useful when using the alternative pattern matching syntax, for example:

let rec factorial = function
    | 0 | 1 -> 1
    | n -> n * factorial (n - 1)

The variable n is a pattern. If the factorial function is called with a 5, the 0 and 1 patterns will fail, but the last pattern will match and bind the value to the identifier n.

Note to beginners: variable binding in pattern matching often looks strange to beginners, however it is probably the most powerful and useful feature of F#. Variable binding is used to decompose data structures into component parts and allow programmers to examine each part; however, data structure decomposition is too advanced for most F# beginners, and the concept is difficult to express using simple types like ints and strings. This book will discuss how to decompose data structures using pattern matching in later chapters.


Using Guards within Patterns[edit]

Occasionally, it's not enough to match an input against a particular value; we can add filters, or guards, to patterns using the when keyword:

let sign = function
    | 0 -> 0
    | x when x < 0 -> -1
    | x when x > 0 -> 1

The function above returns the sign of a number: -1 for negative numbers, +1 for positive numbers, and '0' for 0:

> sign -55;;
val it : int = -1
 
> sign 108;;
val it : int = 1
 
> sign 0;;
val it : int = 0

Variable binding is useful because it's often required to implement guards.

Pay Attention to F# Warnings[edit]

Note that F#'s pattern matching works from top to bottom: it tests a value against each pattern, and returns the value of the first pattern which matches. It is possible for programmers to make mistakes, such as placing a general case above a specific (which would prevent the specific case from ever being matched), or writing a pattern which doesn't match all possible inputs. F# is smart enough to notify the programmer of these types of errors.

Example With Incomplete Pattern Matches

> let getCityFromZipcode zip =
    match zip with
    | 68528 -> "Lincoln, Nebraska"
    | 90210 -> "Beverly Hills, California";;
 
      match zip with
  ----------^^^^
 
stdin(12,11): warning FS0025: Incomplete pattern matches on this expression.
For example, the value '0' will not be matched
 
val getCityFromZipcode : int -> string

While this code is valid, F# informs the programmer of the possible error. F# warns us for a reason:

> getCityFromZipcode 68528;;
val it : string = "Lincoln, Nebraska"
> getCityFromZipcode 32566;;
Microsoft.FSharp.Core.MatchFailureException:
Exception of type 'Microsoft.FSharp.Core.MatchFailureException' was thrown.
   at FSI_0018.getCityFromZipcode(Int32 zip)
   at <StartupCode$FSI_0020>.$FSI_0020._main()
stopped due to error

F# will throw an exception if a pattern isn't matched. The obvious solution to this problem is to write patterns which are complete.

On occasions when a function genuinely has a limited range of inputs, its best to adopt this style:

let apartmentPrices numberOfRooms =
    match numberOfRooms with
    | 1 -> 500.0
    | 2 -> 650.0
    | 3 -> 700.0
    | _ -> failwith "Only 1-, 2-, and 3- bedroom apartments available at this complex"

This function now matches any possible input, and will fail with an explanatory informative error message on invalid inputs (this makes sense, because who would rent a negative 42 bedroom apartment?).

Example With Unmatched Patterns

> let greeting name = 
    match name with
    | "Steve" -> "Hello!"
    | "Carlos" -> "Hola!"
    | _ -> "DOES NOT COMPUTE!"
    | "Pierre" -> "Bonjour";;
 
      | "Pierre" -> "Bonjour";;
  ------^^^^^^^^^
 
stdin(22,7): warning FS0026: This rule will never be matched.
 
val greeting : string -> string

Since the pattern _ matches anything, and since F# evaluates patterns from top to bottom, its not possible for the code to ever reach the pattern "Pierre".

Here is a demonstration of this code in fsi:

> greeting "Steve";;
val it : string = "Hello!"
> greeting "Ino";;
val it : string = "DOES NOT COMPUTE!"
> greeting "Pierre";;
val it : string = "DOES NOT COMPUTE!"

The first two lines return the correct output, because we've defined a pattern for "Steve" and nothing for "Ino".

However, the third line is wrong. We have an entry for "Pierre", but F# never reaches it. The best solution to this problem is to deliberately arrange the order of conditions from most specific to most general.

Note to beginners: The code above contains an error, but it will not throw an exception. These are the worst kinds of errors to have, much worse than an error which throws an exception and crashes an app, because this error puts our program in an invalid state and silently continues on its way. An error like this might occur early in a program's life cycle, but may not show its effects for a long time (it could take minutes, days, or weeks before someone notices the buggy behavior). Ideally, we want buggy behavior to be as "close" to its source as possible, so if a program enters an invalid state, it should throw an exception immediately.
Previous: Values and Functions Index Next: Recursion