BLACK FRIDAY SALE: Save big on all my Swift books and bundles! >>

Key points

Before we get on to the challenges for this project, there are two points I want to explore in more depth to make sure you’ve understood them thoroughly: how map() and filter() fit into the larger world of functional programming, and Swift’s Result type.

Functional programming

Although I cover functional programming a lot in my book Pro Swift, I want to touch on it here too because we used it twice in project 16: once with map() and once with filter(). Both those methods are designed to let us specify what we want rather than how we get there, and both are part of a wider programming approach called functional programming.

To demonstrate how this approach differs from a common alternative, called imperative programming, take a look at this code:

let numbers = [1, 2, 3, 4, 5]
var evens = [Int]()

for number in numbers {
    if number.isMultiple(of: 2) {
        evens.append(number)
    }
}

That creates an array of integers, loops over them one by one, and adds those that are multiples of two to a new array called evens – we need to spell out exactly how we want the process to happen. That code is easy to read, easy to write, and works great, but if we were to rewrite it using filter() we’d get this:

let numbers = [1, 2, 3, 4, 5]
let evens = numbers.filter { $0.isMultiple(of: 2) }

Now we don’t need to spell out how things should happen, and instead focus on what we want to happen: we provide filter() with a test it can perform, and it does the rest. This means our code is shorter, which is awesome, but it’s also improved in three other ways:

  1. It’s no longer possible to put a surprise break inside the loop – filter() will always process every element in the array, and this extra simplicity means we can focus on the test itself.
  2. Rather than providing a closure we can call a shared function instead, which is great for code reuse.
  3. The resulting evens array is now constant, so we can’t modify it by accident afterwards.

Writing less code is always nice, but writing code that is simpler, more reusable, and less variable is even better!

Functions that accept a function as a parameter, or send back a function as their return value, are called higher-order functions, and both map() and filter() are examples of them. Swift has many more like them, but one of the most useful is compactMap(), which:

  1. Runs a transformation function over every item in an array, just like map().
  2. Unwraps any optionals returned by that transformation function, and puts the result into a new array to be returned.
  3. Any optionals that are nil get discarded.

So, while map() will create a new array containing the same number of items as the array it took in, compactMap() might return the same amount, fewer items, or even none at all!

To see the difference between map() and compactMap() in action, try this example:

let numbers = ["1", "2", "fish", "3"]
let evensMap = numbers.map(Int.init)
let evensCompactMap = numbers.compactMap(Int.init)

That creates an array of strings, then converts it to an array of integers using map() then compactMap(). When that code runs, evensMap will contain two optional integers, then nil, then another optional integer, whereas evensCompactMap will contain three real integers – no optionality, and no nil. Much better!

Result

We used Swift’s Result type as a simple way of returning a single value that either succeeded or failed, but there are a few important features I think you’ll find useful in your own code.

First, if you think about it, Result is like a slightly more advanced form of optionals. Optionals either contain some sort of value – an integer, a string, etc – or they contain nothing at all, and Result also contains some sort of value, but now rather than nothing at all for the alternative case it must contain some sort of error.

Behind the scenes, optionals and Result are both implemented as a Swift enum with two cases. For optionals the enum is called Optional and the cases are .none for nil and .some with an associated value of your integer/string/etc, and for Result they are .success with an associated value or .failure with another associated value.

The only real difference between the two is that Swift has syntactic sugar in place for optionals – special syntax designed to make our life easier, because optionals are so common. So, things like if let and optional chaining exist for optionals, whereas Result doesn’t have any special code around it.

Second, as you’ve seen a Result contains some sort of success value or some sort of error value, but if you ever need it there are ways of using Result and throwing functions interchangeably.

If you have a Result and want to get back to do/catch territory, just call the get() method of your Result – this will return the successful value if it exists, or throws its error otherwise.

As an example, consider code like this:

enum NetworkError: Error {
    case badURL
}

func createResult() -> Result<String, NetworkError> {
    return .failure(.badURL)
}

let result = createResult()

That defines some sort of error, creates a function that returns either a string or an error (but in practice always returns an error), then calls that function and puts its return value into result. If you wanted to use do/catch with that value, you could use get() like this:

do {
    let successString = try result.get()
    print(successString)
} catch {
    print("Oops! There was an error.")
}

To go the other way – to create a Result value from throwing code – you’ll find that Result has an initializer that accepts a throwing closure. If the closure returns a value successfully that gets used for the success case, otherwise the thrown error is placed into the failure case.

For example:

let result = Result { try String(contentsOf: someURL) }

In that code, result will be a Result<String, Error> – it doesn’t have a specific kind of Error because String(contentsOf:) doesn’t send one back.

The last thing you should know about Result is that it has functional methods you’re already used to, including map() and mapError(). For example, the map() method looks inside the Result, and transforms the success value into a different kind of value using a closure you specify – for example, it might transform a string into an integer. However, if it finds failure instead, it just uses that directly and ignores your transformation. Alternatively, mapError() transforms the error from one type to another, which can be helpful if you want to homogenize error types in one place.

This is one of the many things to love about functional programming: once you understand the “takes a closure and uses it to transform stuff” nature of map(), you’ll find it exists on arrays, Result, and even Optional!

Hacking with Swift is sponsored by RevenueCat

SPONSORED In-app subscriptions are a pain to implement, hard to test, and full of edge cases. RevenueCat makes it straightforward and reliable so you can get back to building your app. Oh, and it's free if your app makes less than $10k/mo.

Learn more

Sponsor Hacking with Swift and reach the world's largest Swift community!

BUY OUR BOOKS
Buy Pro Swift Buy Pro SwiftUI Buy Swift Design Patterns Buy Testing Swift Buy Hacking with iOS Buy Swift Coding Challenges Buy Swift on Sundays Volume One Buy Server-Side Swift Buy Advanced iOS Volume One Buy Advanced iOS Volume Two Buy Advanced iOS Volume Three Buy Hacking with watchOS Buy Hacking with tvOS Buy Hacking with macOS Buy Dive Into SpriteKit Buy Swift in Sixty Seconds Buy Objective-C for Swift Developers Buy Beyond Code

Was this page useful? Let us know!

Average rating: 4.8/5

 
Unknown user

You are not logged in

Log in or create account
 

Link copied to your pasteboard.