Let's stop filtering for a second

Tjeerd in 't Veen

Tjeerd in 't Veen

— 10 min read

Often there are better ways to handle Swift collections, let’s cover alternatives to regular filtering for more precise solutions.

When we want to remove elements from a collection, we can use the trivial filter method to filter out the elements that we don’t want. However, whenever you start typing filter, I’d like you to stop for a second, and make sure if there aren’t any specialized methods that you can use instead.

Below you can find some alternatives to filter that can better match your use-case. Some of these alternatives express intent better and even come with performance optimizations.

We’ll cover for loops versus filtering, using removeAll(where:), contains(where:), allSatisfy(predicate:), reduce, and first(where:).

Let’s go on the magical journey of filtering in Swift and start building your collection vocabulary.

Generating a deck of cards

We’ll use a deck of cards to demonstrate various filtering techniques. Let’s start by generating this deck of cards.

First, we create the data models, a card exists of a suit and a face, so we’ll create Suit and a Face type, modeled as enums. Enums are well suited—pun intended—for Suit and Face because they communicate that a suit or face can be a single thing at a time.

enum Suit: CaseIterable {
    case clubs
    case diamonds
    case hearts
    case spades
}

enum Face: CaseIterable {
    case two, three, four, five, six, seven, eight, nine, ten
    case jack, king, queen, ace
}

A Card can be a struct, containing both a suit and face. We’ll make sure to conform to CustomStringConvertible to give a card a custom description when we’re printing it.

let card = Card(face: .ace, suit: .spades)
print(card) // ace of spades

struct Card: CustomStringConvertible {
    let face: Face
    let suit: Suit

    var description: String {
        return "\(face) of \(suit)"
    }
}

If you want to learn more about modeling data with enums and structs, check out the free chapter of Swift in Depth, Chapter 2: Modeling data with enums (PDF)

With the data model in place, let’s generate a deck of cards. I’ll demonstrate two different ways. We’ll create a deck mutably using nested for loops and we’ll create this deck immutably using flatMap and map.

With the for loop variant, we’ll have a mutable deck variable, denoted by the var. We’ll iterate through each suit, then for each suit, we’ll iterate through all faces to create a card. Then we shuffle the deck; otherwise our games will get boring.

var deck = [Card]()
for suit in Suit.allCases {
    for face in Face.allCases {
        deck.append(Card(face: face, suit: suit))
    }
}
deck.shuffle()
print(deck) // [ten of hearts, ace of spades, queen of diamonds, ... etc

If a mutable approach isn’t your thing, then below you’ll find an alternative immutable variant that generates a deck using flatMap and map. Notice how deck is a constant, denoted by a let.

We’ll start by iterating over all suits, then inside each iteration, we’ll iterate over all faces. We’ll use map to return a Card. To prevent ending up with a nested array, we use flatMap to flatten the array.

let deck =
    Suit.allCases
        .flatMap { suit in
            Face.allCases.map { face in
                Card(face: face, suit: suit)
            }
        }
        .shuffled()

print(deck) //[seven of diamonds, nine of hearts, ... etc

We call shuffle() on the mutable array to shuffle the array in place. With the immutable variant we call shuffled()—note the extra d—to create a new, shuffled, mutable array.

With a deck in our hands, we’re ready to start filtering using various techniques.

Good ol’ regular filtering

Let’s start easy with the regular filter method, found on the Sequence protocol, and commonly used on arrays, sets, or dictionaries.

A bit counterintuitive, but filter gives us the elements that we do want to keep instead of filtering out the ones we don’t. When filtering coffee, we want to keep and drink the filtered coffee; we do not want to eat the unfiltered coffee-grounds—unless you’re into that. However, in Swift, filtering is the inverse; In Swift, what we’re filtering is what we want to keep.

For instance, if we would like to keep cards that are diamonds, we can filter on a deck of cards and return true in the closure that we pass to filter.

let diamonds = deck.filter { $0.suit == .diamonds}
print(diamonds) // [king of diamonds, ten of diamonds, five of diamonds, ... etc

Alternatively, we can avoid filtering by using a for loop, combined with a where statement. Below we use a for loop while we filter through the deck.

for card in deck where card.suit != .diamonds {
    print(card) // jack of clubs or ten of spades, etc
}

For loop versus filter

Here are some guidelines to help decide between a for loop and filter; filtering is a single action and it iterates through a whole iteration. Filtering also allows for a lazy iteration. A for loop is excellent if you want to do more than just filtering in one loop, or if you want to break, continue, or return out of an iteration.

Inverted filtering with removeAll(where:)

filter is used to signal the elements that we want to keep. If we want to filter elements to remove them, a naive approach could be to use an inverted filter.

Let’s say we want to retain all cards except diamonds, we would inverse the filter.

let everythingButDiamonds = deck.filter { $0.suit != .diamonds }
print(everythingButDiamonds) // [four of hearts, queen of spades, king of spades, etc...

The downside is that this approach copies all the elements to a new array, even if none are removed. Also, our code removes elements that we do not want to keep, whereas filter is by default indicating what we do want to keep.

Luckily, Swift offers a specialized method for this called removeAll(where:). As soon as we have a mutable array (or any other RangeReplaceableCollection such as a string), then we can use this method.

Notice how we apply removeAll on a mutable array, and that we don’t need to invert the comparison anymore. We can state $0.suit == .diamonds instead of $0.suit != .diamonds.

var mutableDeck = deck
mutableDeck.removeAll(where: { $0.suit == .diamonds })
print(mutableDeck) // [eight of spades, five of hearts, ... etc

As a reminder, for both filter and removeAll(where:), the “true” result in the expression is the result that we’d like to communicate. Not only does removeAll(where:) express intent better than an inverted filter, as a cherry on top, removeAll(where:) has increased performance. The downside is that we need a variable, another downside is that removeAll(where:) isn’t available on all collections, such as dictionaries.

var dict = [1: "a", 2: "b"]
dict.removeAll(where: { true }) // error: extra argument 'where' in call

Luckily, removeAll(where:) is available on String, since String also conforms to RangeReplaceableCollection. We could, for instance, remove numbers from a string:

var string = "H2e3l45l6o"
string.removeAll(where: { character in
    character.isNumber
})
print(string) // Hello

Finding an element using first(where:)

To find the first element of a particular type, it might be tempting to use filter.

For instance, let’s say we want to find the first queen in the deck of cards. We can filter for queens and call first.

let firstQueen = deck.filter { $0.face == .queen }.first
print(firstQueen) // Optional(queen of spades)

Using filter to find a first element works, but there is a problem with it. By using filter, we iterate through the whole collection which isn’t needed; We can stop iterating once we found a first of something.

We used first to find the first element in a collection. However, this method is overloaded, which means that there is another first method, and it’s one that accepts a closure; We can use the easily overlooked first(where:) method, which sifts through a collection and returns the first element that it can find. The benefit is that first(where:)immediately stops when we return true, saving us from iterating a full collection.

Below we’ll find the first queen again, but this time we’ll use first(where:) which accepts a closure.

let firstQueen = deck.first({ $0.face == .queen })
print(firstQueen) // Optional(queen of spades)

Checking if an element conforms to a predicate using contains(where:)

When we want to check if a collection has any specific elements, we again might naively be tempted to use filter.

For instance, we could check if our deck of cards has any queens in it. First, we filter on all the queens, then we use the isEmpty check (and a ! before the statement), to make sure that the deck isn’t empty.

let anyQueens = !deck.filter({ $0.face == .queen }).isEmpty
print(anyQueens) // true

There are two problems with this: First, This notation becomes a bit awkward with the two-step checking and the fact that we’re flipping a boolean. It might feel like a strawman argument—“Who would write this?“—but I’ve seen it pop up plenty of times in the wild.

Second, the performance is again an issue. We are iterating through a whole collection with filter. But, once we found a queen, we might as well stop iterating.

As you might have guessed, there is a specialized alternative, and it’s called contains(where:). This method will stop iteration once we return true. On top of this, this method expresses intent better.

Below we again check if a deck has a queen. This time, we’ll use contains(where:) which is much easier to read.

let anyQueens = deck.contains { $0.face == .queen }
print(anyQueens) // true

Checking if all elements conform to a predicate using allSatisfy(predicate:)

A naive, roundabout way to check if all elements conform to a predicate is to filter a collection, and then compare the count of the filtered collection with the unfiltered collection. If the two counts are the same, we know that all elements conform to the predicate.

As an example, we draw 4 cards from the deck into our hand. Then we check if all these cards are an ace. We do this by filtering out everything but an ace and comparing the filtered count with the original count of our hand.

let hand = deck.prefix(4)
let areAllAces = hand.filter({ $0.face == .ace }).count == hand.count
print(areAllAces) // false

One problem with this approach is that it’s a verbose syntax. Second, we are again iterating through a full collection. Once we know a card is not an ace, we can stop iterating, which filter doesn’t allow. Moreover, we are creating a new array using filter, which almost immediately gets discarded.

As a close cousin to contains(where:), we can use allSatisfy(predicate:) to check if all elements conform to a predicate.

Whereas contains(where:) returns true once our passed closure returns true once, allSatisfy(predicate:) returns true if our passed closure returns true for all elements.

Again, we can check if all cards are aces by using allSatisfy(predicate:).

let aces = [
    Card(face: .ace, suit: .clubs),
    Card(face: .ace, suit: .diamonds),
    Card(face: .ace, suit: .hearts),
    Card(face: .ace, suit: .spades)
]

let areAllAces = aces.allSatisfy { $0.face == .ace }
print(areAllAces) // true

It will also return false if any element doesn’t conform to the predicate. Such as checking if all cards are clubs.

let areAllClubs = aces.allSatisfy { $0.suit == .clubs }
print(areAllClubs) // false

The performance benefit is that once our closure returns false, allSatisfy will stop the loop, saving us from iterating through a full collection.

Using reduce instead of filter

reduce is abstract enough to recreate many methods with it, including map. Let’s see how we can use reduce for some advanced iterations including filtering.

Let’s say we want to take 20 cards from a deck, and turn these into a dictionary where the faces are the keys, and suits are the values. But, we don’t want to keep any spades.

We could first try filtering the collection, and then creating a dictionary, but then we’re iterating over the collection twice. Instead, we can combine both the filtering and creation of a dictionary with a single iteration using reduce.

With reduce we can turn a collection into something else, such as a dictionary; reduce works great when we want to perform filtering while turning an array into a dictionary.

Don’t fret if you don’t understand reduce right away, it’s an abstract concept. For more details check out chapter 9 of Swift in Depth

In the next code, we take 20 cards from the deck using prefix, then, we turn the cards into a dictionary where the faces are the keys, and suits are the values. Meanwhile, we are also filtering to make sure that a card is not a spades.

let dictionary =
    deck
        .prefix(20) // draw 20 cards
        .reduce(into: [Face: Suit]()) { dict, card in
            guard card.suit != .spades else { return } // ignore spades

            dict[card.face] = card.suit // build a dictionary
}

After obtaining 20 cards using prefix, we turn the cards array into a filtered dictionary with a single iteration. When you want to bring out the big guns, consider reduce!

Become a Swift pro

Updated for Swift 5!

Check out Swift in Depth and master tough concepts; Such as Generics, Functional Programming concepts, and Protocol-Oriented Programming.

Conclusion

After reading this article, I hope that you’re more aware of the specialized methods that we can use. Next time you write filter, or see it in a pull request, I hope you’ll take a second and replace filter with a better alternative where possible.

Before you go, I recommend automating away some naive filtering approaches by using SwiftLint. For instance, you can set up rules such as using first(where:) over .filter { .. }.first, which are helpful to avoid suboptimal filtering practices.


Written by

Tjeerd in 't Veen has a background in product development inside startups, agencies, and enterprises. His roles included being a staff engineer at Twitter 1.0 and iOS Tech Lead at ING Bank.