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:

  1. They get lost in the jargon (map, bind, apply, functor, monoid, monad and many more)

  2. 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
  1. Here we apply pattern matching on number using the match expression

  2. The expression starts with match to-be-matched with

  3. Multiple cases that we wish to handle, where each case begins with |

  4. On the left side of -> we have the constant value that we are matching

  5. On the right side of -> we have the result expression for the case

  6. The last case _ is the wildcard case, catch-all, default case

  7. We 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:

  1. When working in the functional programming world, think in terms of transformations

  2. A transformation could be anything:

    1. int to string

    2. int to two times the value

    3. list to sum of the list or max or min

    4. student to person

    5. person to person with name in upper case

  3. 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

  4. Start looking at functions and pattern matching as transformations, x to y

  5. 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!