How to Use "where" in Swift
You use the where
keyword to filter things in Swift. Loop over all items where x = true
, for example. It’s simple and powerful! In this tutorial, we’ll discuss the various scenarios in which you can use where
in Swift.
Here’s what we’ll get into:
- What’s “where” and why is it scattered throughout Swift?
- Working with “where” in generics, protocols and extensions
- Using
where
in afor in where
loop - Using
where
in withswitch
andcase
- Filtering and higher-order functions and “where”
Ready? Let’s go.
- Where in Swift
- Using “where” in a For Loop
- Using “where” with Switch
- Generics, Protocols, Extensions and “where”
- Higher-Order Functions and “where”
- Further Reading
Where in Swift
You use where
in Swift to filter things, kinda like a conditional. In various places throughout Swift, the “where” clause provides a constraint and a clear indicator of the data or types you want to work with.
What’s so special about where
– as you’ll soon see – is its flexible syntax. For example, for item in array where item.x == true
will get you all array items for which a condition x == true
returns true. It’s so concise, and so powerful.
What’s intriguing about the where
“keyword” in Swift, is that you can use it in seemingly unrelated places. You can filter a construct like a for in
loop or switch
with “where”, but you can also use it to constrain extensions or generics to a certain type. Moreover, you see “where” also in common higher-order functions like first(where:)
.
Before we discuss how “where” works in these different scenarios, it’s important that you understand that where
in Swift always filters something. “Get me all of X where X.x = y”. When you look at it like this, reading Swift code that uses where
becomes incredibly intuitive – and that’s where
‘s superpower.
Let’s dive in!
Author’s Note: Before I mostly worked on iOS (and Android) apps, I did a lot of web development with PHP and MySQL. You use SQL to get data from databases, and the SQL language includes a construct called a “WHERE clause”. Example: SELECT * FROM fruits WHERE type = 'Apple'
. When you’re reading such a query, it may help to read it right-to-left: Where type is Apple, select (all) fruits. The resulting data is a subset of the whole; a filtered set, which may help you reason better about your code.
Using “where” in a For Loop
The first use case for “where” we’ll discuss is in the for in loop. With the for in
loop you can loop over arrays, ranges, etcetera. You can filter the items to loop over with where
.
Here’s an example:
let primes = [2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37]
for prime in primes where prime < 10 {
print(prime)
}
// Output: 2 3 5 7
In the above code we’ve got a bunch of prime numbers. With the for value in collection where condition
syntax, we’re filtering integers from primes
for which the condition prime < 10
is true
.
What’s interesting is that we can use the prime
constant, as defined in for prime in ···
also in the where
clause itself. This prime
constant refers to the current item in the primes
array. You typically use this inside the body of the loop, between {
and }
.
When you compare the above code that uses where
, with the code below, it becomes clear that using “where” in Swift is syntactic sugar.
for prime in primes {
if prime < 10 {
print(prime)
}
}
// Output: 2 3 5 7
Both these Swift code examples have the same result, but the one with where
is more concise and easier to read. The where
clause “obscures” the uglier syntax, hence “syntactic sugar”.
It’s also good to know that using where
in a for in
loop has viable alternatives. Check this out:
[2, 3, 5, 7, 11, 13, 17, 19, 23, 29].filter { $0 < 10 }.map { print($0) }
// Output: 2 3 5 7
The above code uses the filter(_:)
and map(_:)
higher-order functions to filter and transform the array of numbers. Again, the result is the same but the implementation is different. It’s up to you to choose which alternative is best in your own code.
You can use any kind of expression for a where
clause, including conditionals like >
greater than, type checking with is
, and functions like isMultiple(of:)
.
Using “where” with Switch
You can also use where
with switch
. You use the switch statement in Swift to match a value with one of several patterns. You can use where
to define an additional condition for these patterns.
Let’s take a look at an example:
enum Item {
case weapon(Int, Int)
case potion(Int)
case armor(Int, Int, Double)
}
let item = Item.weapon(75, 9)
switch item {
case .weapon(let hitPoints, _) where hitPoints > 50:
player.slowAttack(hitPoints) // Heavy weapon attack
case .weapon(let hitPoints, let weight):
player.attack(hitPoints, speed: weight * 10)
case .potion(let health):
player.health += health
case .armor(let damage, let weight, let condition):
player.damageThreshold = damage * condition
}
In the above code we’ve created a scenario for an imaginary Role Playing Game (RPG). You’ve got an item
in your inventory, which can be a weapon, a potion or an armor. The Item
enum uses associated values to define stats like hitpoints, damage and health.
In the switch
block, we’re determining what happens for each of the item types. A weapon is used to attack, for example, and a potion is used to increase the health of the player. Each of these scenarios and actions are coded within the switch
and case
statements.
This case
is special, though:
case .weapon(let hitPoints, _) where hitPoints > 50:
It defines an additional condition which must be true
for the case to match. Consider that you have a .weapon
assigned to item
. One of 2 things is going to happen:
- If the weapon’s first associated value,
hitPoints
, is greater than50
, the first case – the one withwhere
– will match and is executed - If the weapon’s
hitPoints
value is less than or equal to50
, i.e. not greater than 50, the secondcase
matches and is executed
See how that works? It’s like combining a case
pattern match with an additional condition. It lets you get super clear about what kind of pattern you want to match, under which conditions. And just as before, you can use any kind of expression with where
. Neat!
Next to for in
and switch
, you can also use where
with guard let
, while
, if let
and do-catch
, in a similar fashion. Keep in mind that when where
filters, it’ll also exclude something. In case of catch
, for example, you’ll want to ensure you catch some or all errors.
Generics, Protocols, Extensions and “where”
In the previous sections, we’ve looked at how you can use where
to apply a filter in the context of values, like arrays. You can, however, also use where
to apply filters in the context of types. Let’s dive in!
Check out the following extension in Swift:
extension Array where Element == String {
func reverseAll() -> [String] {
return self.map { String($0.reversed()) }
}
}
let names = ["Arthur", "Ford", "Zaphod", "Trillian"]
print(names.reverseAll())
// Output: ["ruhtrA", "droF", "dohpaZ", "naillirT"]
In the above code, we’ve defined an extension for the Array
type. This adds a function reverseAll()
to Swift’s pre-existing type for arrays. The function itself will call reversed()
on every array item.
Logically, the reversed()
function only works on arrays of strings. That’s why we’ve constrained the extension with where Element == String
. You can read this as: Only add this extension’s functions for array’s whose elements are of type String
.
The code makes use of the Element
generic placeholder, which is a “stand in” for the actual type of an array. That’s a concept that belongs to generics, not where
clauses, but it’s important to point out here.
In the above code, we’re directly comparing types with the equals operator ==
. This works well for concrete types like String
, but what if you want to constrain an extension to, say, a protocol?
Check this out:
extension Sequence where Element: Hashable {
func unique() -> Set<Element> {
var uniques = Set<Element>()
for item in self {
uniques.insert(item)
}
return uniques
}
}
let numbers = [1, 1, 3, 9, 22, 3, 4, 5, 22, 9]
print(numbers.unique())
What’s going on here?
First off, the unique()
function takes a crude approach to removing duplicates from any kind of sequence, like an array of numbers. It does so by relying on a characteristic of the Set type, namely, that any value in a Set
must be unique.
Comparing if two elements in a Set
are equal happens by comparing their hashes. Any type that has a hash – it is-hashable – conforms to the Hashable
protocol. The unique()
function simply adds all items to a Set
, and the insert(_:)
won’t add an already existing item to the set if its hash is already present.
Based on the requirement that the items in the sequence must be hashable, we can define a constraint for the extension of where Element: Hashable
. This means that the Element
of the sequence, such as String
for an array of strings, must conform to the Hashable
protocol. As such, we can only use the unique()
function on sequences, collections, arrays, etc. whose items conform to Hashable
. Neat!
Why go through all this trouble to remove some duplicates from an array of numbers? Well, these examples are trivial of course. They’re merely here to explain the principle, nothing more. In real life though, imagine you’ve got a bunch of custom objects like User
or Tweet
. You need to remove duplicates in a few places in your code, which is why you “abstract away” the deduplicating function into an extension constrained to specific but flexible types.
Higher-Order Functions and “where”
So far we’ve looked at using where
to filter data and types, but there’s a third spot where you’ll encounter where clauses: in higher-order functions. In this usage scenario, where
isn’t syntax, but a convention for function names and arguments.
Take a look at the following example:
let names = ["Arthur", "Ford", "Zaphod", "Trillian"]
if let name = names.first(where: { $0.contains("a") }) {
print(name)
}
// Output: Zaphod
In the above code, we’re trying to find a string in an array of strings that contains an “a”. We’re doing so with the first(where:)
function. This is a higher-order function, which means it’ll take a closure as an argument for the function itself.
We’re applying the closure { $0.contains("a") }
to every item in the names
array. As soon as the closure returns true
for a string in the array, that string is returned. We’re wrapping this in an optional binding block with if let
, because first(where:)
naturally returns an optional value.
There’s more of them, by the way:
-
removeAll(where:)
removes all elements for which the closure returnstrue
-
contains(where:)
returns all elements for which the closure returnstrue
-
first(where:)
returns the first value for which the closure returnstrue
-
firstIndex(where:)
is similar tofirst(where:)
except it returns the index - Same goes for
last(where:)
andlastIndex(where:)
, which returns the last match
What is where
in these functions? In this code, “where” is an argument label of the first(where:)
function. Unlike before, with where Element == Type
. This “where” convention is followed throughout Swift, so you know you’re dealing with a function that filters with a where clause. Awesome!
Further Reading
OK, before you go, check this out:
let names = ["Arthur", "Ford", "Zaphod", "Trillian"]
for name in names where name.contains("a") {
print(name)
break
}
// Output: Zaphod
We’ve come full circle! The above code has the same result as the one with first(where:)
, and they both use “where”, but they’ve got completely different implementations. Goes to show that coding Swift is often more about finding the right implementation, than about finding the right result.
Want to learn more? Check out these resources:
- Map, Reduce and Filter in Swift
- FlatMap and CompactMap Explained in Swift
- How To Find an Item in an Array in Swift
- Generics in Swift Explained
- Extensions in Swift Explained
- Protocols in Swift Explained
- Enums in Swift Explained
- Switch Statements in Swift Explained
- Arrays in Swift Explained
- Sets in Swift Explained