In the previous article, we saw the basics of pattern matching using switch
on enums
. But what about using switch
with anything other than enum
types?
This post is part of an article series. You can read all the parts here: part 1, part 2, part 3, part 4
Pattern matching with tuples
In Swift, you’re not restricted to use switch
on integer values or enums like in ObjC.
You can actually switch
on a lot of stuff, including (but not restricting to) tuples.
This means that you can check multiple data at once by grouping them in a tuple. For example, imagine you have a CGPoint
and want to check if it’s on a special axis, you can switch
on its .x
and .y
properties!
let point = CGPoint(x: 7, y: 0)
switch (point.x, point.y) {
case (0,0): print("On the origin!")
case (0,_): print("x=0: on Y-axis!")
case (_,0): print("y=0: on X-axis!")
case (let x, let y) where x == y: print("On y=x")
default: print("Quite a random point here.")
}
Notice the use of the wildcard _
we saw in the previous article, as well as the (let x, let y)
in the fourth case
which binds the variables to then be able to use a where
to check if both are equal, also as we saw in the previous article.
Cases are evaluated in order
Also notice that the switch
statement will evaluate its case
patterns in the order they are defined, and will stop at the first one which matches. Contrary to C and Objective-C, there is no need for a break
keyword1.
This means that in the code above, if the point is (0,0)
, then it will match the first case
and print "On the origin!"
, but it will stop there, without matching with (0,_)
nor with (_,0)
, even if those case
patterns would otherwise match. Because it stops at the first match.
Strings and Characters
But why stop at tuples? In Swift you can also switch
to a lot of native types, including String
and Character
for example:
let car: Character = "J"
switch car {
case "A", "E", "I", "O", "U", "Y": print("Vowel")
default: print("Consonant")
}
Here you can also notice that you can use multiple patterns separated by a comma to execute the same code for multiple matches (namely all the vowels here). This allows you to avoid code repetition easily.
Ranges
Ranges are also usable for pattern matching. As a reminder, a Range<T>
is a generic type which contains a start
and end
both of type T
, where T
must be a ForwardIndexType
. This includes types like Int
and Character
.
💡 In Swift 2, you can declare a range either explicitly using Range(start: 1900, end: 2000)
, or using the syntactic sugar operators ..<
(range excluding its end
) and ...
(range including its end
); e.g. you can declare the same range as above using 1900..<2000
. The latter form (using the ..<
operator) is preferred though, both because it’s more readable and because Range(start:end:)
will be removed in Swift 3.0 anyway.
So how do we use that with switch
? Well easy, use ranges in your case
patterns to check if a value is within that range!
let count = 7
switch count {
case Int.min..<0: print("Negative count, really?")
case 0: print("Nothing")
case 1: print("One")
case 2..<5: print("A few")
case 5..<10: print("Some")
default: print("Many")
}
Here you see that we mixed cases
with a single Int
value and cases
with Range<Int>
values. That’s not a problem as long as all possible cases are covered by your switch
.
Even if using Int
for ranges is the most common case, we can also do that with other ForwardIndexType
, including… Character
! Remember the code above? The problem is that it printed “Consonant” even for punctuation characters and anything other than A-Z
. So let’s solve that2 (and also include lowercase vowels and consonants):
func charType(_ car: Character) -> String {
switch car {
case "A", "E", "I", "O", "U", "Y", "a", "e", "i", "o", "u", "y":
return "Vowel"
case "A"..."Z", "a"..."z":
return "Consonant"
default:
return "Other"
}
}
print("Jules Verne".characters.map(charType))
// ["Consonant", "Vowel", "Consonant", "Vowel", "Consonant", "Other", "Consonant", "Vowel", "Consonant", "Consonant", "Vowel"]
Types
Ok but can we go further? Well of course we can: let’s also use pattern matching… on types!
For this example, let’s define 3 structs all conforming to the same protocol:
protocol Medium {
var title: String { get }
}
struct Book: Medium {
let title: String
let author: String
let year: Int
}
struct Movie: Medium {
let title: String
let director: String
let year: Int
}
struct WebSite: Medium {
let url: NSURL
let title: String
}
// And an array of Media to switch onto
let media: [Medium] = [
Book(title: "20,000 leagues under the sea", author: "Jules Verne", year: 1870),
Movie(title: "20,000 leagues under the sea", director: "Richard Fleischer", year: 1955)
]
Then how do we switch
according to the type of Medium
and do a different thing for Book
than for Movie
? Easy, use as
and is
for pattern matching!
for medium in media {
// The title part of the protocol, so no need for a switch there
print(medium.title)
// But for the other properties, it depends on the type
switch medium {
case let b as Book:
print("Book published in \(b.year)")
case let m as Movie:
print("Movie released in \(m.year)")
case is WebSite:
print("A WebSite with no date")
default:
print("No year info for \(medium)")
}
}
Notice we use as
for Book
and Movie
here, because we want both to see if they match the type, and if they do, store the casted type in a constant (let b
or let m
), because we then want to use it3.
On the other hand, we only used is
for WebSite
because we only want to check if medium
can match the pattern of “being a WebSite
”. But if it does, we don’t need to type-cast it and use the type-casted value (we don’t use it in the print
statement). That’s a bit like if we did write case let _ as WebSite
, as we don’t care about the WebSite
object as long as it’s of that type.
💡 Note: having to use as
and is
like this in a switch
might sometimes reveal a code-smell, e.g. in that particular use case it’d probably have been better to add a var releaseInfo: String { get }
property to the protocol Medium
instead of switch
-ing over the various types.
What’s next?
In the upcoming parts we’ll look at how to make your own types be directly usable for pattern matching, explore some more syntactic sugar, then look at some pattern matching usages outside of the switch
statement and some even more complex pattern expressions… can’t wait!
Note: I’m going to be traveling ✈️ to Japan 🇯🇵 in the next two weeks 🤗, so might not be able to publish the next parts of this article series as quickly as the previous ones, but I won’t forget you!
⏩ Read next part of this article series here: part 3
Thanks to Frank Manno for updating the code samples of this article to Swift 3!
-
You can override this behavior using the
fallthrough
keyword, to let the evaluation continue through the nextcase
. But in practice this is very rarely useful and not often encountered. ↩ -
Of course, that’s not the best and recommended way to implement such a string analysis feature — as Unicode characters and locales are way more complex than that. So instead for such a feature we should probably use
NSCharacterSet
, consider what are the letters that the currentNSLocale
consider as vowels (is “y” a vowel? What about “õ” or “ø”?), etc. So don’t take that example too seriously as it’s only a sample to illustrate the power ofswitch
+Range
. ↩ -
despite the similarity with expressions like
if let b = medium as? Book
— where you also bind a variable ifmedium
can be casted to the expected type — be careful to useas
and notas?
in a pattern matching expression. Even if the mechanics might seem similar, the semantics are different here (“try type-casting andnil
if it fails” vs. “check if the pattern of considering it of this type does match or not”). ↩