Articles, podcasts and news about Swift development, by John Sundell.

Transforming collections in Swift

Published on 21 Jan 2018
Basics article available: Map, FlatMap and CompactMap

Almost every Swift program uses collections in one way or another. Whether it's to store values to be displayed in some form of list, to keep track of observers, or caching data - collections are everywhere.

When working with collections, it's very common to use the same dataset over a sequence of operations, and continuously transform it into new values. For example, we might download some JSON data, then transform it into an array of dictionaries and finally into a collection of models.

This week, let's take a look at some of the standard library APIs that lets us easily transform collections in a very functional way.

Mapping and flatMapping

Let's start with the basics - using map and flatMap. They both let us transform a collection into another using a function or closure, with the difference that flatMap automatically skips nil values.

What's nice is that both of these transformation APIs are rethrowing, meaning that they will automatically throw any error that was generated during the transformation process (and they won't be throwing at all in case the transform closure itself isn't throwing).

Let's say that we want to extend the Bundle class to give us a nice and easy way to load an array of files by name. To do that we could write a for-loop that iterates over each name and collects the loaded files into a mutable array, but what's the fun in that? 😉

Let's use map and flatMap instead - to build ourselves a nice chain of transformations:

extension Bundle {
    func loadFiles(named fileNames: [String]) throws -> [File] {
        return try fileNames
            // Since flatMap returns a new sequence of all non-nil
            // values returned from its closure, it lets us automatically
            // skip all files that don't exist.
            .flatMap({ name in
                return url(forResource: name, withExtension: nil)
            })
            .map(Data.init)
            .map(File.init)
    }
}

Above we also take advantage of Swift's first class function capabilities (that we took a look at 2 weeks ago), by passing Data and File's initializer as a closure to the map function.

The result is a more declarative setup where each transformation that we want to perform on our collection is very clearly defined. However, one might argue that it also hurts readability a bit, especially for developers who might not be very familiar with this type of concepts.

Let's take a look at how we can possibly improve the readability of these kinds of transformations, while still using everything that the standard library has to offer - by taking a look at the reduce API.

Reducing

The reduce method enables you to reduce a collection into a single value. Rather than transforming into a new collection (like when using map and flatMap), you end up with a single result based on applying a closure on every element in the collection.

Let's say that we're building a game, and at the end of each session we'd like to calculate the player's total score, by iterating over all Level models in the game and adding up their score.

Again, this could be done with a mutable Int and a for-loop, but just like when using map and flatMap we can perform this operation in one go using reduce, which lets us avoid any mutable state.

With reduce, you pass an initial value to start from, and a function that returns a new value by taking the current one and transforming it, like this:

extension Game {
    func calculateTotalScore() -> Int {
        return levels.reduce(0) { result, level in
            // On each iteration we take the previous result
            // and add the current level's score.
            return result + level.score
        }
    }
}

Of all the transformation APIs that the standard library has to offer, reduce is arguably the hardest one to understand, and can sometimes make for a bit confusing code. Just by looking at the above, it kind of looks like we're trying to reduce the number 0, which doesn't make much sense 😅.

Let's take a look at how we could possibly improve the above code's readability. One way to do it could be to start by mapping our levels' scores into a new Int array before calling reduce, which lets us simply pass the + operator as a closure (since now we are expected to pass a (Int, Int) -> Int closure, which the + operator is):

extension Game {
    func calculateTotalScore() -> Int {
        return levels.map({ $0.score }).reduce(0, +)
    }
}

A bit nicer, but we can still do better! Since the (by far) most common use case of reduce is to add up numbers, how about adding a sum function to Sequence that lets us pass in a property that in turn will be used with reduce, like this:

extension Sequence {
    func sum<N: Numeric>(by valueProvider: (Element) -> N) -> N {
        return reduce(0) { result, element in
            return result + valueProvider(element)
        }
    }
}

With the above extension in place we can now simply pass the property that we want to sum up, leaving us with a very nice and clean call site:

extension Game {
    func calculateTotalScore() -> Int {
       return levels.sum { $0.score }
    }
}

Pretty nice, right? 😀

Zipping

Finally, let's take a look at how we can use zip to combine two sequences into one. This is particularly useful when you have two sequences and you're not sure if their element count matches up.

Let's say we want to render a ViewModel in one of our views, and we have an array of ImageViews that we'll render a sequence of images using. If we were to use a classic for loop we'd have to do bounds-checking to make sure that we are not accessing an out-of-bounds index in one of the arrays:

func render(_ viewModel: ViewModel) {
    for (index, imageName) in viewModel.imageNames.enumerated() {
        let image = UIImage(named: imageName)

        // Since we might have more images than we have place for
        // in the UI, we need to add this bounds-check.
        guard index < imageViews.count else {
            return
        }

        imageViews[index].image = image
    }
}

If we instead use zip, we get bounds-checking for free. The iteration will only continue as long as both of the sequences has a matching element for each index, so we can simply write our iteration like this:

func render(_ viewModel: ViewModel) {
    let images = viewModel.imageNames.flatMap(UIImage.init)

    for (image, imageView) in zip(images, imageViews) {
        imageView.image = image
    }
}

Very nice and clean 👍

Conclusion

Using functional transformations on sequences and collections can be a bit of a double-edged sword. On one hand, it can let you drastically reduce the amount of code you need to write to perform a series of transformations, but on the other hand it can make your code a bit harder to read as well.

Like always, it becomes a balancing act of deciding when or when not to deploy this kind of features, and using extensions to add your own convenience APIs on top of the standard library can also be a great solution.

An added bonus is that, due to Swift's protocol-oriented nature, all of the above APIs will also work on any custom collections that you might create. For more information on that, check out "Creating custom collections in Swift".

What do you think? Do you like to use this kind of operations to transform your collections, or is it something that you'll try out? Let me know, along with any questions, comments or feedback you have - on Twitter @johnsundell.

Thanks for reading! 🚀