Disclaimers
- I’m on the path of learning these complex topics, so if you find a mistake, please let me know at @lintuxt
- 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.
- The post assumes some basic knowledge about
if/else
andswitch
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 …
- The
moveRover(_:String)
function is expecting aString
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. 😅 - The
moveRover(_:String)
function is repeating theif/else
structure on one variable, the direction variable, which it can be defined as anEnum
. - 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.