released Thu, 20 Aug 2015
Swift Version 5.1

Advanced Pattern Matching

In this post, we'll have a look at Pattern Matching in Swift in terms of the 'switch', 'for', 'if', and 'guard' keywords. We'll have a look at the basic syntax and at best practices and helpful examples.

Introduction

Swift's switch statement bears little resemblance to the similarly named equivalent in C or Objective-C. Even though they share the same name, the Swift version can do much, much more.

In the following guide, I will try to explain the various usages for these new features in more detail.

The main feature of switch is, of course, pattern matching: the ability to destructure values and match different switch cases based on correct match of the values to the cases.

Destructure

Destructuring means to take something of a certain structure and destructure it into smaller items again. Imagaine you had a lnk::tuple variable user with the following value: (firstname: "Harry", lastname: "Potter", age: 21, occupation: "Wizard")

Destructuring means taking this tuple and converting it into individual variables:

let harry = (firstname: "Harry", lastname: "Potter", age: 21, occupation: "Wizard")

let (name, surname, age, occupation) = harry
print(surname)

Destructuring is a great method for handling the information in complex types. It is also a fundamental part of Swift's switch statement. The next step, then, is to have a look at switch:

A simple example

We wrote a game where both players have to take a quiz. After each question we evaluate who won or lost. There're four states:

  • Player 1 is correct
  • Player 2 is correct
  • Both were correct
  • Both were wrong

We can model this logic very nicely with a switch statement such as the following:

let player1 = true
let player2 = false
switch (player1, player2) {
case (true, false): print("Player 1 won")
case (false, true): print("Player 2 won")
case (true, true): print("Draw, both won")
case (false, false): print("Draw, both lost")
}

Here, we create a tuple (player1, player2) and then match each of the possible cases.

This was a very short introduction, now we will go into more detail.

A Trading Engine

So a Wall Street company contacts you, they need a new trading platform running on iOS devices. As it is a trading platform, you define an enum for trades.

First Draft

enum Trades {
    case buy(stock: String, amount: Int, stockPrice: Float)
    case sell(stock: String, amount: Int, stockPrice: Float)
}

You were also handed the following API to handle trades. Notice how sell orders are just negative amounts.

/**
 - parameter stock: The stock name
 - parameter amount: The amount, negative number = sell, positive = buy
*/
func process(stock: String, _ amount: Int) {
    print ("\(amount) of \(stock)")
}

The next step is to process those trades. You see the potential for using pattern matching and write this:

let aTrade = Trades.buy(stock: "APPL", amount: 200, stockPrice: 115.5)

switch aTrade {
case .buy(let stock, let amount, _):
    process(stock, amount)
case .sell(let stock, let amount, _):
    process(stock, amount * -1)
}
// Prints "buy 200 of APPL"

Swift lets us conveniently only destructure / extract the information from the enum that we really want. In this case only the stock and the amount.

Awesome, you visit Wall Street to show of your fantastic trading platform. However, as always, the reality is much more cumbersome than the beautiful theory. Trades aren't trades you learn.

  • You have to calculate in a fee which is different based on the trader type.
  • The smaller the institution the higher the fee.
  • Also, bigger institutions get a higher priority.

They also realized that you'll need a new API for this, so you were handed this:

func processSlow(stock: String, _ amount: Int, _ fee: Float) { print("slow") }
func processFast(stock: String, _ amount: Int, _ fee: Float) { print("fast") }

Trader Types

So you go back to the drawing board and add another enum. The trader type is part of every trade, too.

enum TraderType {
case singleGuy
case company
} 

enum Trades {
    case buy(stock: String, amount: Int, stockPrice: Float, type: TraderType)
    case sell(stock: String, amount: Int, stockPrice: Float, type: TraderType)
}

So, how do you best implement this new restriction? You could just have an if / else switch for buy and for sell, but that would lead to nested code which quickly lacks clarity - and who knows maybe these Wall Street guys come up with further complications. So you define it instead as additional requirements on the pattern matches:


let aTrade = Trades.sell(stock: "GOOG", amount: 100, stockPrice: 666.0, type: TraderType.company)

switch aTrade {
case let .buy(stock, amount, _, TraderType.singleGuy):
    processSlow(stock, amount, 5.0)
case let .sell(stock, amount, _, TraderType.singleGuy):
    processSlow(stock, -1 * amount, 5.0)
case let .buy(stock, amount, _, TraderType.company):
    processFast(stock, amount, 2.0)
case let .sell(stock, amount, _, TraderType.company):
    processFast(stock, -1 * amount, 2.0)
}

The beauty of this is that there's a very succinct flow describing the different possible combinations. Also, note how we changed .buy(let stock, let amount) into let .buy(stock, amount) in order to keep things simpler. This will destructure the enum just as before, only with less syntax.

Guards! Guards!

Once again you present your development to your Wall Street customer, and once again a new issue pops up (you really should have asked for a more detailed project description).

  • Sell orders exceeding a total value of $1.000.000 do always get fast handling, even if it's just a single guy.
  • Buy orders under a total value of $1.000 do always get slow handling.

With traditional nested if syntax, this would already become a bit messy. Not so with switch. Swift includes guards for switch cases which allow you to further restrict the possible matching of those cases.

You only need to modify your switch a little bit to accommodate for those new changes


let aTrade = Trades.buy(stock: "GOOG", amount: 1000, stockPrice: 666.0, type: TraderType.singleGuy)

switch aTrade {
case let .buy(stock, amount, _, TraderType.singleGuy):
    processSlow(stock, amount, 5.0)
case let .sell(stock, amount, price, TraderType.singleGuy)
    where price*Float(amount) > 1000000:
    processFast(stock, -1 * amount, 5.0)
case let .sell(stock, amount, _, TraderType.singleGuy):
    processSlow(stock, -1 * amount, 5.0)
case let .buy(stock, amount, price, TraderType.company)
    where price*Float(amount) < 1000:
    processSlow(stock, amount, 2.0)
case let .buy(stock, amount, _, TraderType.company):
    processFast(stock, amount, 2.0)
case let .sell(stock, amount, _, TraderType.company):
    processFast(stock, -1 * amount, 2.0)
}

This code is quite structured, still rather easy to read, and wraps up the complex cases quite well.

That's it, we've successfully implemented our trading engine. However, this solution still has a bit of repetition; we wonder if there're pattern matching ways to improve upon that. So, let's look into pattern matching a bit more.

Pattern Types

So now we've seen several patterns in action. But what's the syntax here? Which other things can we match for? Swift distinguishes 7 different patterns. We're going to have a quick look at each of them.

All of those patterns can not only be used with the switch keyword, but also with the if, guard, and for keywords. For details on this, read on.

Wildcard Pattern

The wildcard pattern ignores the value to be matched against. In this case any value is possible. This is the same pattern as let _ = fn() where the _ indicates that you don't wish to further use this value. The interesting part is that this matches all values including nil.

You can also match optionals by appending a ? to make it _?:

let p: String? = nil
switch p {
// Any value is possible, but only if the optional has a value
case _?: print ("Has String")
// Only match the empty optional case
case nil: print ("No String")
}

As you've seen in the trading example, it also allows you to omit the data you don't need from matching enums or tuples:

switch (15, "example", 3.14) {
// We're only interested in the last value
case (_, _, let pi): print ("pi: \(pi)")
}

Identifier Pattern

Matches a concrete value. This is how things work in Objective-C's switch implementation:

Here, we have a special case just for the number 5

let number = 4
switch number {
case 5: print("it is a 5")
default: print("it is something else")
}

We can also match strings, see our code below to greet people in their native language.

let language = "Japanese"
switch name {
case "Japanese": print("")
case "English": print("Hello!")
case "German": print("Guten Tag")
}

Value-Binding Pattern

This is the very same as binding values to variables via let or var. Only in a switch statement. You've already seen this before, so I'll provide a very short example:

switch (4, 5) {
case let (x, y): print("\(x) \(y)")
}

The let (x, y) in the example above will take the values of our (4, 5) tuple and write them into two new variables named x and y.

We can easily combine this with the other pattern matching operations to develop very powerful patterns. Imagine you have a function that returns an optional tuple (username: String, password: String)?. You'd like to match it and make sure if the password is correct:

First, our fantastic function (just a prototype):

func usernameAndPassword() 
    -> (username: String, password: String)? {... }

Now, the switch example:

switch usernameAndPassword() {
case let (_, password)? where password == "12345": login()
default: logout()
}

See how we combined multiple Swift features here, we will go through them step by step:

  1. We use case let to create new variables
  2. We use the ? operator to only match if the optional return value from the usernameAndPassword function is not empty.
  3. We ignore the username part via _, because we're only interested in the password
  4. We use where to make sure our highly secure password is correct
  5. We use default for all the other cases that fail.

Tuple Pattern

We have a full article on tuples, but here is a quick overview:

let age = 23
let job: String? = "Operator"
let payload: Any = NSDictionary()

switch (age, job, payload) {
case (let age, _?, _ as NSDictionary):
    print(age)
default: ()
}

Here, we're combining three values into a tuple (imagine they're coming from different API calls) and matching them in one go. Note that the pattern achieves three things:

  1. It extracts the age
  2. It makes sure there is a job, even though we don't need it
  3. It makes sure that the payload is of kind NSDictionary even though we don't need the actual value either.

Enumeration Case Pattern

As you saw in our trading example, pattern matching works really great with Swift's enums. That's because enum cases are like sealed, immutable, destructable structs. Much like with tuples, you can unwrap the contents of an individual case right in the match and only extract the information you need.

Imagine you're writing a game in a functional style and you have a couple of entities that you need to define. You could use structs but as your entities will have very little state, you feel that that's a bit of an overkill.

enum Entities {
    case soldier(x: Int, y: Int)
    case tank(x: Int, y: Int)
    case player(x: Int, y: Int)
}

Now you need to implement the drawing loop. Here, we only need the X and Y position:

for e in entities() {
    switch e {
    case let .soldier(x, y):
      drawImage("soldier.png", x, y)
    case let .tank(x, y):
      drawImage("tank.png", x, y)
    case let .player(x, y):
      drawImage("player.png", x, y)
    }
}

This is the gist of it. The enumeration case pattern is really just using enum cases in the switch statement.

Type Casting Pattern

As the name already implies, this pattern casts or matches types. It has two different keywords:

  • is type: Matches the runtime type (or a subclass of it) against the right hand side. This performs a type cast but disregards the returned type. So your case block won't know about the matched type.
  • pattern as type: Performs the same match as the is pattern but for a successful match casts the type into the pattern specified on the left hand side.

Here is an example of the two.

let a: Any = 5 
switch a {

// this fails because a is still Any
// error: binary operator '+' cannot be applied to operands of type 'Any' and 'Int'
case is Int: print (a + 1)

// This works and returns '6'
case let n as Int: print (n + 1)

default: ()
}

Note that there is no pattern before the is. It matches directly against a.

Expression Pattern

The expression pattern is very powerful. It matches the switch value against an expression implementing the ~= operator. There're default implementations for this operator, for example for ranges, so that you can do:

switch 5 {
case 0..10: print("In range 0-10")
default: print("In another range")
}

However, the much more interesting possibility is overloading the operator yourself in order to add matchability to your custom types. Let's say that you decided to rewrite the soldier game we wrote earlier and you want to use structs after all.

struct Soldier {
  let hp: Int
  let x: Int
  let y: Int
}

Now you'd like to easily match against all entities with a health of 0. We can simply implement the ~= operators as follows.

func ~= (pattern: Int, value: Soldier) -> Bool {
    return pattern == value.hp
}

Now we can match against an entity. In this example, only soldiers that have a hp of 0 would be matched (thus, we print dead soldier), because we're commparing the value.hp to the switch pattern in our ~= implementation above.

let soldier = Soldier(hp: 99, x: 10, y: 10)
switch soldier {
case 0: print("dead soldier")
default: ()
}

What if you'd like to not just compare the hp but also the x and the y? You can just implement pattern with a tuple:

func ~= (pattern: (hp: Int, x: Int, y: Int), value: Soldier) -> Bool {
    let (hp, x, y) = pattern
    return hp == value.hp && x == value.x && y == value.y
}


let soldier = Soldier(hp: 99, x: 10, y: 10)
switch soldier {
case (50, 10, 10): print("health 50 at pos 10/10")
default: ()
}

You can even match structs against structs. However, this only works if your structs are Equatable. Swift can implement this automatically, as long as you tell it to by conforming to the protocol. So lets first extend our Soldier struct to conform to Equatable:

struct Soldier: Equatable {
    let hp: Int
    let x: Int
    let y: Int
}

Now, we can add a new match implementation. Since both soldiers are equatable value types, we can actually just directly compare them. If they both have the same values for their three properties (hp, x, y), then they are considered equal:

func ~= (pattern: Soldier, value: Soldier) -> Bool {
    return pattern == value
}

let soldier = Soldier(hp: 50, x: 10, y: 10)
switch soldier {
case Soldier(hp: 50, x: 10, y: 10): print("The same")
default: ()
}

The left side of the ~= operator (the pattern argument) can be anything. So it can even be a protocol:

protocol Entity {
    var value: Int {get}
}

struct Tank: Entity {
    var value: Int
    init(_ value: Int) { self.value = value }
}

struct Peasant: Entity {
    var value: Int
    init(_ value: Int) { self.value = value }
}

func ~=(pattern: Entity, x: Entity) -> Bool {
    return pattern.value == x.value
}

switch Tank(42) {
case Peasant(42): print("Matched") // Does match
default: ()
}

There's a lot of things you can do with Expression Patterns. For a much more detailed explanation of Expression Patterns, have a look at this terrific blog post by Austin Zheng.

This completes list of possible switch patterns. Our next topic is flow control in switch statements.

Fallthrough, Break and Labels

The following is not directly related to pattern matching but only affects the switch keyword, so I'll keep it brief. By default, and unlike C/C++/Objective-C, switch cases do not fall through into the next case which is why in Swift, you don't need to write break for every case. If you never used Objective-C or C and this confuses you, here's a short example that would print "1, 2, 3":

/* This is C Code */
switch (2) {
case 1: printf("1");
case 2: printf("2");
case 3: printf("3");
}

You would need to use case 1: printf("1"); break; in order to not automatically fall through into the next case.

Fallthrough

In Swift, it is the other way around. If you actually want to fall through into the other case, you can opt into this behaviour with the fallthrough keyword.

switch 5 {
case 5:
 print("Is 5")
 fallthrough
default:
 print("Is a number")
}
// Will print: "Is 5" "Is a number"

This only works, if your switch cases do not establish let variables, because then Swift would not know what to do.

Break

You can use break to break out of a switch statement early. Why would you do that if there's no default fallthrough? For example if you can only realize within the case that a certain requirement is not met and you can't execute the case any further:

let userType = "system"
let userID = 10
switch (userType, userID)  {
case ("system", _):
  guard let userData = getSystemUser(userID) 
     else { break }
  print("user info: \(userData)")
  insertIntoRemoteDB(userData)
default: ()
}
... more code that needs to be executed

Here, we don't want to call insertIntoRemoteData when the result from getSystemUser is nil. Of course, you could just use an if let here, but if multiple of those cases come together, you quickly end up with a bunch of horrifyingly ugly nested if lets.

Labels

But what if you execute your switch in a while loop and you want to break out of the loop, not the switch? For those cases, Swift allows you to define labels to break or continue to:

gameLoop: while true {
  switch state() {
  case .waiting: continue gameLoop
  case .done: calculateNextState()
  case .gameOver: break gameLoop
  }
}

See how we explicitly tell Swift in the gameOver case that it should not break out of the switch statement but should break out of the gameLoop instead.

We've discussed the syntax and implementation details of switch and pattern matching. Now, let us have a look at some interesting (more or less) real world examples.

Real World Examples

Swift's pattern matching is very helpful in order to write cleaner code. Sometimes it can be a bit tricky to rethink common programming patterns in a way that makes them applicable to pattern matching. This guide will help you along by introducing various real world examples that clearly benefit from pattern matching.

Optionals

There're many ways to unwrap optionals, and pattern matching is one of them. You've probably used that quite frequently by now, nevertheless, here's a short example:

var result: String? = secretMethod()
switch result {
case nil:
    print("is nothing")
case let a?:
    print("\(a) is a value")
}

As you can see, result could be a string, but it could also be nil. It's an Optional. By switching on result, we can figure out whether it is .none or whether it is an actual value. Even more, if it is a value, we can also bind this value to variable right away. In this case a. What's beautiful here, is the clearly visible distinction between the two states, that the variable result can be in.

Type Matches

Given Swift's strong type system, there's usually no need for runtime type checks like it more often happens in Objective-C. However, when you interact with legacy Objective-C code (which hasn't been updated to reflect simple generics yet), then you often end up with code that needs to check for types. Imagine getting an array of NSStrings and NSNumbers:

let u = NSArray(array: [NSString(string: "String1"), NSNumber(int: 20), NSNumber(int: 40)])

When you go through this NSArray, you never know what kind of type you get. However, switch statements allow you to easily test for types here:

for item in array {
    switch item {
    case is NSString:
        print("string")
    case is NSNumber:
        print("number")
    default:
        print("Unknown type \(item)")
    }
}

Applying ranges for grading

So you're writing the grading iOS app for your local Highschool. The teachers want to enter a number value from 0 to 100 and receive the grade character for it (A - F). Pattern Matching to the rescue:

let aGrade = 84

switch aGrade {
case 90...100: print("A")
case 80...90: print("B")
case 70...80: print("C")
case 60...70: print("D")
case 0...60: print("F")
default: print("Incorrect Grade")
}

You can also always have ranges as parts of nested types, such as tuples or even struct types, when you implement the ~= operator.

let student = (name: "John Donar", grades: (english: 77, chemistry: 21, math: 60, sociology: 42))
switch student {
case (let name, (90...100, 0...50, 0...50, _)): print("\(name) is good at arts")
case (let name, (0...50, 90...100, 90...100, _)): print("\(name) is good at sciences")
default: ()
}

Word Frequencies

We have a sequence of pairs, each representing a word and its frequency in some text. Our goal is to filter out those pairs whose frequency is below or above a certain threshold, and then only return the remaining words, without their respective frequencies.

Here're our words:

let wordFreqs = [("k", 5), ("a", 7), ("b", 3)]

A simple solution would be to model this with map and filter:

let res = wordFreqs.filter({ (e) -> Bool in
    if e.1 > 3 {
        return true
    } else {
        return false
    }
}).map { $0.0 }
print(res)

However, with compactMap a map that only returns the non-nil elements, we can improve a lot upon this solution. First and foremost, we can get rid of the e.1 and instead have proper destructuring by utilizing (you guessed it) tuples. And then, we only need one call compactMap instead of filter and then map which adds unnecessary performance overhead.

let res = wordFreqs.compactMap { (e) -> String? in
    switch e {
    case let (s, t) where t > 3: return s
    default: return nil
    }
}
print(res)

Directory Traversion

Imagine you want to traverse a file hierachy and find:

  • all "psd" files from customer1 and customer2
  • all "blend" files from customer2
  • all "jpeg" files from all customers.
guard let enumerator = FileManager.default.enumeratorAtPath("/customers/2014/")
    else { return }

for url in enumerator {
    switch (url.pathComponents, url.pathExtension) {

    // psd files from customer1, customer2
    case (let f, "psd") 
            where f.contains("customer1") || f.contains("customer2"): print(url)

    // blend files from customer2
    case (let f, "blend") 
            where f.contains("customer2"): print(url)

    // all jpg files
    case (_, "jpg"): print(url)

    default: ()
    }
}

Note that contains stops at the first match and doesn't traverse the complete path. Again, pattern matching lead to very succinct and readable code.

Fibonacci

Also, see how beautiful an implementation of the fibonacci algorithm looks with pattern matching.

func fibonacci(i: Int) -> Int {
    switch(i) {
    case let n where n <= 0: return 0
    case 0, 1: return 1
    case let n: return fibonacci(n - 1) + fibonacci(n - 2)
    }
}

print(fibonacci(8))

Since we're doing recursion here, this will fail to work with sufficiently large numbers (you'll see the dreaded stack overflow error)

Legacy API and Value Extractions

Oftentimes, when you get data from an external source, like a library, or an API, it is not only good practice but usually even required that you check the data for consistency before interpreting it. You need to make sure that all keys exists or that the data is of the correct type, or the arrays have the required length. Not doing so can lead from buggy behaviour (missing key) to crash of the app (indexing non-existent array items). The classic way to do this is by nesting if statements.

Let's imagine an API that returns a user. However, there're two types of users: System users - like the administrator, or the postmaster - and local users - like "John B", "Bill Gates", etc. Due to the way the system was designed and grew, there're a couple of nuisances that API consumers have to deal with:

  • system and local users come via the same API call.
  • the department key may not exist, since early versions of the db did not have that field and early employees never had to fill it out.
  • the name array contains either 4 items (username, middlename, lastname, firstname) or 2 items (full name, username) depending on when the user was created.
  • the age is an Integer with the age of the user

Our system needs to create user accounts for all system users from this API with only the following information: username, department. We only need users born before 1980. If no department is given, "Corp" is assumed.

func legacyAPI(id: Int) -> [String: Any] {
    return ["type": "system", "department": "Dark Arts", "age": 57, 
           "name": ["voldemort", "Tom", "Marvolo", "Riddle"]] 
}

Given these constraints, let's develop a pattern match for it:

let item = legacyAPI(4)
switch (item["type"], item["department"], item["age"], item["name"]) {
case let (sys as String, dep as String, age as Int, name as [String]) where 
  age < 1980 &&
  sys == "system":
  createSystemUser(name.count == 2 ? name.last! : name.first!, dep: dep ?? "Corp")
default:()
}

// returns ("voldemort", "Dark Arts")

Note that this code makes one dangerous assumption, which is that if the name array does not have 2 items, it must have 4 items. If that case doesn't hold, and we get a zero item name array, this would crash.

Other than that, it is a nice example of how pattern matching even with just one case can help you write cleaner code and simplify value extractions.

Also, see how we're writing let at the beginning right after the case, and don't have to repeat it for each assignment within the case.

Patterns with other Keywords

The Swift documentation points out, that not all patterns can be used with the if, for or the guard statement. However, the docs seem to be outdated. All 7 patterns work for all three keywords.

As a shorter example, see the Value Binding, Tuple, and Type Casting pattern used for all three keywords in one example:

// This is just a collection of keywords that compiles. This code makes no sense
func valueTupleType(a: (Int, Any)) -> Bool {
    // guard case Example
    guard case let (x, _ as String) = a else { return false}
    print(x)

    // for case example
    for case let (a, _ as String) in [a] {
        print(a)
    }

    // if case example
    if case let (x, _ as String) = a {
       print("if", x)
    }

    // switch case example
    switch a {
    case let (a, _ as String):
        print(a)
        return true
    default: return false
    }
}

With this in mind, we will have a short look at each of those keywords in detail.

Using for case

Let's write a simple array function which only returns the non-nil elements

func nonnil<T>(array: [T?]) -> [T] {
   var result: [T] = []
   for case let x? in array {
      result.append(x)
   }
   return result
}

print(nonnil(["a", nil, "b", "c", nil]))

The case keyword can be used in for loops just like in switch cases. Here's another example. Remember the game we talked about earlier? Well, after the first refactoring, our entity system now looks like this:

enum Entity {
    enum EntityType {
        case soldier
        case player
    }
    case Entry(type: EntityType, x: Int, y: Int, hp: Int)
}

Fancy, this allows us to draw all items with even less code:

for case let Entity.Entry(t, x, y, _) in gameEntities()
where x > 0 && y > 0 {
    drawEntity(t, x, y)
}

Our one line unwraps all the necessary properties, makes sure we're not drawing beyond 0, and finally calls the render call (drawEntity).

In order to see if the player won the game, we want to know if there is at least one Soldier with health > 0

func gameOver() -> Bool {
    for case Entity.Entry(.soldier, _, _, let hp) in gameEntities() 
    where hp > 0 {return false}
    return true
}
print(gameOver())

What's nice is that the .soldier match is part of the for query. This feels a bit like SQL and less like imperative loop programming. Also, this makes our intent clearer to the compiler, opening up the possibilities for dispatch enhancements down the road. Another nice touch is that we don't have to spell out Entity.EntityType.soldier. Swift understands our intent even if we only write .soldier as above.

Using guard case

Another keyword which supports patterns is the newly introduced guard keyword. You know how it allows you to bind Optionals into the local scope much like if let only without nesting things:

func example(a: String?) {
    guard let a = a else { return }
    print(a)
}
example("yes")

guard let case allows you to do something similar with the power that pattern matching introduces. Let's have a look at our soldiers again. We want to calculate the required HP until our player has full health again. Soldiers can't regain HP, so we should always return 0 for a soldier entity.

let MAX_HP = 100

func healthHP(entity: Entity) -> Int {
    guard case let Entity.Entry(.player, _, _, hp) = entity 
      where hp < MAX_HP 
        else { return 0 }
    return MAX_HP - hp
}

print("Soldier", healthHP(Entity.Entry(type: .soldier, x: 10, y: 10, hp: 79)))
print("Player", healthHP(Entity.Entry(type: .player, x: 10, y: 10, hp: 57)))

// Prints:
"Soldier 0"
"Player 43"

This is a beautiful example of the culmination of the various mechanisms we've discussed so far.

  • It is very clear, there is no nesting involved
  • Logic and initialization of state are handled at the top of the func which improves readability
  • Very terse.

This can also be very successfully combined with switch and for to wrap complex logical constructs into an easy to read format. Of course, that won't make the logic any easier to understand, but at least it will be provided in a much saner package. Especially if you use enums.

Using if case

if case can be used as the opposite of guard case. It is a great way to unwrap and match data within a branch. In line with our previous guard example. Obviously, we need an move function. Something that allows us to say that an entity moved in a direction. Since our entities are enums, we need to return an updated entity.

func move(entity: Entity, xd: Int, yd: Int) -> Entity {
    if case Entity.Entry(let t, let x, let y, let hp) = entity
    where (x + xd) < 1000 &&
        (y + yd) < 1000 {
    return Entity.Entry(type: t, x: (x + xd), y: (y + yd), hp: hp)
    }
    return entity
}
print(move(Entity.Entry(type: .soldier, x: 10, y: 10, hp: 79), xd: 30, yd: 500))
// prints: Entry(main.Entity.EntityType.soldier, 40, 510, 79)

Finishing Words

Pattern Matching is a really powerful feature of Swift. It is one of the defining features that makes Swift Code so much nicer to read than what you'd write in Objective-C. It is also deeply integrated into the language as it can be used with let, guard, for, if, and switch. It also allows adding where clauses, using is or as for type casts, lets you extend it via ~= and much more.

Similar Articles