F Sharp Programming/Pattern Matching Basics
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
editIn 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
editPattern 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
You can add additional parameters to the definition:
let getPrice taxRate = function
| "banana" -> 0.79 * (1.0 + taxRate)
| "watermelon" -> 3.49 * (1.0 + taxRate)
| "tofu" -> 1.09 * (1.0 + taxRate)
| _ -> nan (* nan is a special value meaning "not a number" *)
val getPrice : taxRate:float -> _arg1:string -> float
Calling this gives:
> getPrice 0.10 "tofu";;
val it : float = 1.199
Note that the added parameters in the call come before the implicit parameter against which the match is made.
Comparison to Other Languages
editF#'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) => n switch
{
0 => 0,
1 => 1,
_ => Fib(n - 1) + Fib(n - 2)
};
Like all functions, pattern matches can only have one return type.
Binding Variables with Pattern Matching
editPattern 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
int
s andstring
s. This book will discuss how to decompose data structures using pattern matching in later chapters.
Using Guards within Patterns
editOccasionally, 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
editNote 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. To prevent this sort of problem it is usually a good idea to set the compiler flag that treats all warnings as errors; then the code will not compile thus preventing the problem right at the beginning.