Basics 09 - Pattern Matching (Basics)
Love and hate relationship with programming. Always learning programming and Jazz.
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
numberusing thematchexpressionThe expression starts with
match to-be-matched withMultiple 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:
inttostringintto 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!