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
fallthroughkeyword, 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 currentNSLocaleconsider 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 ifmediumcan be casted to the expected type — be careful to useasand notas?in a pattern matching expression. Even if the mechanics might seem similar, the semantics are different here (“try type-casting andnilif it fails” vs. “check if the pattern of considering it of this type does match or not”). ↩