Disclaimers

  1. I’m on the path of learning these complex topics, so if you find a mistake, please let me know at @lintuxt
  2. The ideas, concepts, stories, and examples of this post are ALL a product of my invention. Any similarities with reality are just a pure coincidence.
  3. The post assumes some basic knowledge about if/else and switch statements in Swift.

Pattern Matching in Swift

Pattern Matching is one of the flagships of the Functional Programming paradigm, and Swift has you covered. I’ll try to make this blog post as entertaining as possible to explain the concept.

Let’s imagine a hypothetical function that moves a Rover on Mars …

moveRover(_:String)

func moveRover(to direction: String) {
    if direction == "North" {
        ...
    } else if direction == "South" {
        ...
    } else if direction == "East" {
        ...
    } else if direction == "West" {
        ...
    }
}

Now, to make it easier, let’s define some move() functions and plug them in the code above.

func moveNorth() { print("moveNorth() was called.") }
func moveEast() { print("moveEast() was called.") }
func moveSouth() { print("moveSouth() was called.") }
func moveWest() { print("moveWest() was called.") }

func moveRover(to direction: String) {
    if direction == "North" {
        moveNorth()
    } else if direction == "South" {
        moveSouth()
    } else if direction == "East" {
        moveEast()
    } else if direction == "West" {
        moveWest()
    }
}

Do you see a problem with the moveRover(_:String) function?

Take a moment to reflect on the code above before diving into the next section.

Well, there are some things to mention …

  1. The moveRover(_:String) function is expecting a String as a parameter to know in which direction to move. Now, imagine if a programmer calls this function with "Earth" as a parameter. What would happen? No one knows, but in the best-case scenario, the Rover will not move. 😅
  2. The moveRover(_:String) function is repeating the if/else structure on one variable, the direction variable, which it can be defined as an Enum.
  3. The moveRover(_:String) function only allow us to move in just 4 directions (or 90 degrees increments).

1. Solving the moveRover(_:String)’s String parameter problem

The main problem with the moveRover(_:String) function is that the compiler can’t help us in detecting some human errors. As I mentioned earlier, if the programmer uses "Earth" or "north"as a parameter, the outcome is “undefined.” However, what if we could tell the compiler that "Earth" or any other String is not a valid parameter for this function. Well, it turns out there’s a way to do that. Enter Enumerations or Enums for short.

In a nutshell, an Enum is an easy and convenient way to define a restricted Type in Swift which can leverage the type-safety check of the compiler.

So let’s update our code to the following.

Convert moveRover(_:String) to moveRover(_:Direction)

/* 
    moveNorth() {...} 
    moveEast() {...} 
    moveSouth() {...} 
    moveWest() {...} 
*/

enum Direction {
    case North
    case East
    case South
    case West
}

func moveRover(to direction: Direction) {
    if direction == .North {
        moveNorth()
    } else if direction == .East {
        moveEast()
    } else if direction == .South {
        moveSouth()
    } else if direction == .West {
        moveWest()
    }
}

The enum Direction {...} solved the problem of accepting random Strings as parameters. So now, only the pre-defined directions are possible.

2. Avoiding the use of multiple if/else clauses.

The moveRover(_:Direction) is repeating the if/else structure multiple times with all the disadvantages that this kind of code carries. So, now that we have our enum Direction {...} we can do a magic trick. The switch statement allow us to do this …

func moveRover(to direction: Direction) {
    switch direction {
    case .North:
        moveNorth()
    case .South:
        moveSouth()
    case .East:
        moveEast()
    case .West:
        moveWest()
    }
}

Any switch statement in Swift must be exhaustive. In this case, a default case is not required because the direction parameter/variable is an enum that can be easily verified by the compiler. We’re starting to grasp the compiler’s magic here …

3. The Rover can only move in four directions.

The current moveRover(_:Direction)function allows the Rover to move in just four directions. This looks kind of limited since we can’t specify absolute coordinates in our system. So, let’s add a twist to our function.

First, let’s add some new move() functions to our list …

moveNorth() {...} 
moveNorthEast() {...} // New
moveEast() {...} 
moveSouthEast() {...} // New
moveSouth() {...}
moveSouthWest() {...} // New
moveWest() {...}
moveNorthWest() {...} // New

Then, let’s add a new case None to our enum Direction { … }

enum Direction {
    case North
    case East
    case South
    case West
    case None
}

Finally, let’s put everything together.

func moveRover(to direction: Direction, then adjustment: Direction) {
    switch (direction, adjustment) {
    case (.North, .None):
        moveNorth()
    case (.North, .East):
        moveNorthEast()
    case (.East, .None):
        moveEast()
    case (.South, .East):
        moveSouthEast()
    case (.South, .None):
        moveSouth()
    case (.South, .West):
        moveSouthWest()
    case (.West, .None):
        moveWest()
    case (.North, .West):
        moveNorthWest()
    default:
        print("Oops 404, direction not found.")
    }
}

This redefined function specifies a new set of permitted directions (with increments of 45 degrees instead of 90). Now, if the programmer sends the wrong parameter(s), the compiler will be able to catch it. Invalid directions are not allowed anymore. What is even better, the combinations of undefined directions are not even possible. E.g. (.North, .South). Last but not least, I’m oversimplifying the implementation of the 45 degrees increments for the sake of the example but in the end the moveRover(_:Direction,_:Direction) dispatcher function is programmatically correct and can’t be misused by a distracted programmer.

One more thing …

The code above is nice, but we can make it even better.

typealias Vector = (Direction, Direction)

func moveRover(to direction: Vector) {
    switch (direction) {
    case (.North, .None):
        moveNorth()
    case (.North, .East):
        moveNorth()
        moveEast()
    case (.East, .None):
        moveEast()
    case (.South, .East):
        moveSouth()
        moveEast()
    case (.South, .None):
        moveSouth()
    case (.South, .West):
        moveSouth()
        moveWest()
    case (.West, .None):
        moveWest()
    case (.North, .West):
        moveNorth()
        moveWest()
    default:
        print("Oops 404, direction not found.")
    }
}

Using the typealias keyword, I’ve just defined a Vector type which is a tuple of (Direction, Direction) in disguise. So, now the switch statement can go back to our original variable, direction and the moveRover(_:Direction) function.

And without even realizing it …

… you’ve just learned the main concept of Pattern Matching.