Now that we’ve revisited the various syntaxes for pattern matching in part 1, part 2 and part 3, let’s finish this blog post series with some advanced syntax using if case let
, for case where
and all!
Let’s use what we saw in previous articles and apply them all to some advanced expressions.
This post is part of an article series. You can read all the parts here: part 1, part 2, part 3, part 4
if case let
The case let x = y
pattern allows you to check if y
does match the pattern x
.
Writing if case let x = y { … }
is strictly equivalent to writing switch y { case let x: … }
: it’s just a more compact syntax which is useful when you only want to pattern-match against one case — as opposed to a switch
which is more adapted to multiple cases matching.
For example, let’s use an enum
similar to the one from the previous articles:
enum Media {
case book(title: String, author: String, year: Int)
case movie(title: String, director: String, year: Int)
case website(urlString: String)
}
Then we can write this1:
let m = Media.movie(title: "Captain America: Civil War", director: "Russo Brothers", year: 2016)
if case let Media.movie(title, _, _) = m {
print("This is a movie named \(title)")
}
This is equivalent to the more verbose code:
switch m {
case let Media.movie(title, _, _):
print("This is a movie named \(title)")
default: () // do nothing, but this is mandatory as all switch in Swift must be exhaustive
}
if case let where
We can combine the if case let
with a comma (,
) – where each condition is separated by ,
– to create a multi-clause condition:
if case let Media.movie(_, _, year) = m, year < 1888 {
print("Something seems wrong: the movie's year is before the first movie ever made.")
}
That can lead to quite powerful expressions that would otherwise need a complex switch
and multiple lines only to test one specific case.
guard case let
Of course, guard case let
is similar to if case let
. You can use guard case let
and guard case let … , …
to ensure something matches a pattern and a condition and exit otherwise.
enum NetworkResponse {
case response(URLResponse, Data)
case error(Error)
}
func processRequestResponse(_ response: NetworkResponse) {
guard case let .response(urlResp, data) = response,
let httpResp = urlResp as? HTTPURLResponse,
200..<300 ~= httpResp.statusCode else {
print("Invalid response, can't process")
return
}
print("Processing \(data.count) bytes…")
/* … */
}
for case
Combining for
and case
can also let you iterate on a collection conditionally. Using for case …
is semantically similar to using a for
loop and wrapping its whole body in an if case
block: It will only iterate and process the elements that match the pattern.
let mediaList: [Media] = [
.book(title: "Harry Potter and the Philosopher's Stone", author: "J.K. Rowling", year: 1997),
.movie(title: "Harry Potter and the Philosopher's Stone", director: "Chris Columbus", year: 2001),
.book(title: "Harry Potter and the Chamber of Secrets", author: "J.K. Rowling", year: 1999),
.movie(title: "Harry Potter and the Chamber of Secrets", director: "Chris Columbus", year: 2002),
.book(title: "Harry Potter and the Prisoner of Azkaban", author: "J.K. Rowling", year: 1999),
.movie(title: "Harry Potter and the Prisoner of Azkaban", director: "Alfonso Cuarón", year: 2004),
.movie(title: "J.K. Rowling: A Year in the Life", director: "James Runcie", year: 2007),
.website(urlString: "https://en.wikipedia.org/wiki/List_of_Harry_Potter-related_topics")
]
print("Movies only:")
for case let Media.movie(title, _, year) in mediaList {
print(" - \(title) (\(year))")
}
/* Output:
Movies only:
- Harry Potter and the Philosopher's Stone (2001)
- Harry Potter and the Chamber of Secrets (2002)
- Harry Potter and the Prisoner of Azkaban (2004)
- J.K. Rowling: A Year in the Life (2007)
*/
for case where
Adding a where
clause to that all can make it even more powerful:
print("Movies by C. Columbus only:")
for case let Media.movie(title, director, year) in mediaList where director == "Chris Columbus" {
print(" - \(title) (\(year))")
}
/* Output:
Movies by C. Columbus only:
- Harry Potter and the Philosopher's Stone (2001)
- Harry Potter and the Chamber of Secrets (2002)
*/
💡 Note: Using for … where
without the case
pattern matching part is also a valid Swift syntax. For example you can write:
for m in listOfMovies where m.year > 2000 { … }
It is not using pattern matching (no case
nor ~=
) so that’s a bit out of the scope of this article series, but it’s still totally valid and as useful as the other constructs presented here — as it avoids wrapping the whole body of your for
within a big if
(or starting it with a guard … else { continue }
).
Combining them all
Let’s finish this series with the Grand Finale: combine all that we learned from the beginning (including some syntactic sugar like x?
we learned in the previous article):
extension Media {
var title: String? {
switch self {
case let .book(title, _, _): return title
case let .movie(title, _, _): return title
default: return nil
}
}
var kind: String {
// Remember part 1 where we said we can omit the `(…)` associated values in the `case` if we don't care about any of them?
switch self {
case .book: return "Book"
case .movie: return "Movie"
case .website: return "Web Site"
}
}
}
print("All mediums with a title starting with 'Harry Potter'")
for case let (title?, kind) in mediaList.map({ ($0.title, $0.kind) })
where title.hasPrefix("Harry Potter") {
print(" - [\(kind)] \(title)")
}
This look might look a little complex, so let’s split it down:
- It uses
map
to transform theArray<Media>
arraymediaList
into an array of tuples[(String?, String)]
containing the title (if any) + the kind of item (as text) - It only matches if
title?
matches — which is syntactic sugar to say if.Some(title)
matches — the$0.title
of each media. This means that it discards any media for which$0.title
returnsnil
(a.k.a.Optional.None
) — excluding anyWebSite
in the process, as those don’t have anytitle
) - Then it filters the results to only iterate on those for which
title.hasPrefix("Harry Potter")
is true.
So in the end this will loop on every medium that has a title starting with “Harry Potter”, discarding any medium that don’t have a title — like WebSite
— as well as any medium having a title that doesn’t start with "Harry Potter"
— excluding the J.K. Rowling documentary from that iteration as well.
The code will thus output this, only listing Harry Potter books and movies:
All medium with a title starting with 'Harry Potter'
- [Book] Harry Potter and the Philosopher's Stone
- [Movie] Harry Potter and the Philosopher's Stone
- [Book] Harry Potter and the Chamber of Secrets
- [Movie] Harry Potter and the Chamber of Secrets
- [Book] Harry Potter and the Prisoner of Azkaban
- [Movie] Harry Potter and the Prisoner of Azkaban
Using neither pattern matching nor any where
clause nor syntactic sugar that we learned in this article series, the code might have looked like this instead:
print("All mediums with a title and starting with 'Harry Potter'")
for media in mediaList {
guard let title = media.title else {
continue
}
guard title.hasPrefix("Harry Potter") else {
continue
}
print(" - [\(media.kind)] \(title)")
}
Some might find it more readable, but you can’t argue that using for case let (title?, kind) in … where …
is really powerful and allow you to impress your friends make great use of for loops + pattern matching + where
clauses altogether. ✨
Conclusion
This is the end of this “Pattern Matching” series. Hope you enjoyed it and learned some interesting stuff 😉
Next articles will be more focused back on some nice Swifty design patterns and architecture than on Swift syntax and language.
💡 Don’t hesitate to tell me on Twitter if you have any particular subject on Swift you want me blog about and give me some ideas for what to write about next!
Thanks to Frank Manno for updating the code samples of this article to Swift 3!
-
The order of arguments (pattern vs. variable) in that syntax can be troubling. To remember the order, just think of it as using the same
case let Media.movie(…)
syntax you use in aswitch
. That way, you’ll remember to writeif case let Media.movie(…) = m
instead ofif case let m = Media.movie(…)
which wouldn’t compile anyway — grouping thecase
with the pattern (Media.movie(title, _, _)
) like you do inswitch
, and not with the variable to compare it to (m
). ↩