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:
- We use
case let
to create new variables - We use the
?
operator to only match if the optional return value from theusernameAndPassword
function is not empty. - We ignore the
username
part via_
, because we're only interested in thepassword
- We use
where
to make sure our highly secure password is correct - 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:
- It extracts the age
- It makes sure there is a job, even though we don't need it
- 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 yourcase
block won't know about the matched type.- pattern
as
type: Performs the same match as theis
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
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
andlocal
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 |
---|