Basics 09 - Pattern Matching (Basics)
Statement vs. Expression
Very simply put, a statement is about doing something... do this, and then do this, then take a left turn and then do this. Imperative programming, or should I say the thought process, is all about micro-instructions. These instructions are of a wide variety, including comparison, branching, jumps, assignment, function calls, throws/catches etc.
An expression on the other side is something that can be squeezed down to a value. You solve the expression, you are left with a value. What you see on the right side of let x =
is an expression.
let x = 1 + 2 + 3 + 4 + 5
The functional programming thought process is all (nearly all) about expressions... That's the fundamental difference between the two worlds. One world deals with micro-instructions, while the other deals with expressions.
Programmers who have a background in imperative languages find functional languages extremely difficult to understand. Primarily for 2 reasons:
They get lost in the jargon (map, bind, apply, functor, monoid, monad and many more)
They try to apply the statement-driven mindset in a functional language
Take this example (which I copied from MSDN):
let squareAndAddOdd values =
values
|> List.filter (fun x -> x % 2 <> 0)
|> List.map (fun x -> x * x + 1)
let numbers = [ 1; 2; 3; 4; 5 ]
let result = squareAndAddOdd numbers
The next 3 lines of code after let squareAndAddOdd values =
, which can be written in one line as values |> List.filter (fun x -> x % 2 <> 0) |> List.map (fun x -> x * x + 1)
is one giant expression, made up of smaller expressions. Everything on the right side of let squareAndAddOdd values =
is squeezed down to a single value. That value is returned by squareAndAddOdd
function. Later, after the function call, that value is assigned to result
.
While learning and practicing F#, every time you are about to write a statement, a command, or an instruction, take a break. Remind yourself that you need to figure a way out to use expressions, not micro-instructions. Here is a simple example in C#:
static bool IsEven(int num)
{
if (num % 2 == 0)
return true;
else
return false;
}
// Better version
static bool IsEven(int num)
{
return (num % 2 == 0) ? true : false;
}
// Even better version
static bool IsEven(int num) => (num % 2 == 0) ? true : false;
Why Expressions?
Looking at the C# example given above, you may wonder why to worry about statements vs. expressions. What's the big deal? Syntactic non-sense?
The answer is: expressions are fundamentally different from statements in one aspect. Like functions (refer to Functions Part 1), expressions give you the capability to compose bigger expressions. Statements, on the other side, are terminal points. After a statement starts another statement. If you need composability and the ability to pipeline things, you need expressions.
Here is food for thought: C# uses throw
statement for exceptions. In one of the recent versions of C# throw
expression was added. Why?
Now, let's see what pattern matching is.
Pattern Matching
Pattern matching is one of the key features in F# (or most of the functional languages for that matter). Pattern matching, in its entirety, requires a lengthy discussion with many examples. At the moment we are going to focus on the basics, and some use cases.
Here is a simple problem statement. You have a number. If the number equals 1 print "one", if the number equals 2 print "two", else print "unknown".
Here is an imperative pseudo-code implementation for this:
x = input
if x == 1
print "one"
else if x == 2
print "two"
else
print "unknown"
The above implementation is based on comparisons and branching. That's not what we want.
If we want to solve this in a functional style, we need functions, expressions, and composition and/or pipeline of these two. Let's look at various examples/approaches to solve this.
let number = <whatever>
let result =
match number with
| 1 -> "one"
| 2 -> "two"
| _ -> "unknown"
printf "%s" result
Here we apply pattern matching on
number
using thematch
expressionThe expression starts with
match to-be-matched with
Multiple cases that we wish to handle, where each case begins with
|
On the left side of
->
we have the constant value that we are matchingOn the right side of
->
we have the result expression for the caseThe last case
_
is the wildcard case, catch-all, default caseWe have three cases here and the result expression for each case is a string value
The above-given code can also be written as:
let number = <whatever>
match number with
| 1 -> "one"
| 2 -> "two"
| _ -> "unknown"
|> printfn "%s"
Here the output of match
expression is pipelined to printfn
.
If this match
expression is required more than once, you can create a function:
let numberToString num =
match num with
| 1 -> "one"
| 2 -> "two"
| _ -> "unknown"
let number = <whatever>
number |> numberToString |> printfn "%s"
Remember, lambda expressions can be used to replace functions, so:
let number = <whatever>
number
|> fun x ->
match x with
| 1 -> "one"
| 2 -> "two"
| _ -> "unknown"
|> printfn "%s"
And one more. When you have a lambda expression with a single parameter, which does pattern matching, the shorthand syntax can be used:
let number = <whatever>
number
|> function
| 1 -> "one"
| 2 -> "two"
| _ -> "unknown"
|> printf "%s"
In the above example, function
keyword is the replacement for fun x -> match x with
. Functions/lambda expressions with a single parameter for pattern matching are a very common occurrence in F#. Using function
keyword reduces the amount of boilerplate code.
Transformations
Consider the following example:
let stringToInt str = System.Int32.Parse str
let intToString i = i.ToString()
"1" |> stringToInt |> intToString |> printfn "%s"
The last line of code is a series of transformations and then printfn
. Input is passed to stringToInt
, the output of which is passed to intToString
, and then printfn
.
If you replace intToString
in the pipeline with a pattern match:
"1"
|> stringToInt
|> function
| 1 -> "one"
| _ -> "not one"
|> printfn "%s"
You still have a series of transformations.
Conclusion
Here are some ideas that you may allow to marinate in your mind:
When working in the functional programming world, think in terms of transformations
A transformation could be anything:
int
tostring
int
to two times the valuelist to sum of the list or max or min
student to person
person to person with name in upper case
For very few problems (performance-centric code, or a low-level layer for providing an abstract interface to the filesystem, network etc.) you need to use imperative techniques
Start looking at functions and pattern matching as transformations, x to y
Try to solve problems by breaking them down into steps/phases, and stitch those steps/phases using transformations
If you have reached so far, congratulations.
Keep reading!