Map-reduce on iOS, part 2

By Will Braynen

As part 2 to my previous post, this post gets us closer to real industry applications, at least within the mobile space and the space of experience APIs.

In my previous post, we saw this Swift example:

[5,5].map { a in a * 2 } // [5,5] -> [10,10]

Go ahead, try it in Xcode’s Playground! Or, equivalently:

[5,5].map { $0 * 2 } // [5,5] -> [10,10]

This transformed [5,5] into [10,10]. This is a clean example, but I have never had to do that in industry. But what I have had to do in industry is something that is very similar to the following.

Given

// MARK: - Types

struct Book {
  let numberOfPages: Int
  let title: String
  let author: String
}

// MARK: - Properties

let book1 = Book(
  numberOfPages: 304,
  title: "Battleborn",
  author: "Claire Veye Watkins"
)

let book2 = Book(
  numberOfPages: 248,
  title: "Her Body and Other Parties",
  author: "Carmen Maria Machado"
)

let book3 = Book(
  numberOfPages: 240,
  title: "Dance of the Happy Shades",
  author: "Alice Munro"
)

let books = [book1, book2, book3]

The question

What’s the total number of pages? Or rather: how would you programmatically compute the total number of pages for any array of Books?

A right solution

Given what we saw in my previous post, we can now answer The Question by first creating an array that, for each book, only contains page counts (the map step) and then adding up the individual books' page counts (the reduce step). In other words, instead of an array with the books, we would just want [304, 248, 240]. Then, we would want to add up the pages: 304+248+240. Here goes:

let totalPages = books.map { $0.numberOfPages }.reduce(0, +)
annotated.png

You could go a little slower and write this one liner as a two liner:

let pages = books.map { $0.numberOfPages } // [304,248,240]
let totalPages = pages.reduce(0, +) // 792

And if you want the mean (colloquially referred to as the average), then all you have to do is divide it by books.count

This is a different application of map because it feels more like extraction rather than transformation. The extraction of something buried one level deep in the struct instances. But it is perfectly valid nonetheless and can also be combined with reduce.

The wrong solution

For loops (noun, stress on “for” instead of “loops”). I mean, it’s a solution, but it’s kind of old school. It might not be parallelizable, which would be a pity because otherwise a smart compiler could cut computation time by the number of cores available at runtime. And, debatably, it might be less declarative and so more difficult to understand for someone familiar with map-reduce.

Other cool things you can do in swift

This steps outside map-reduce, but still cool. If you wanted to know the title of the shortest and longest books, then you could do it like so:

let shortestBook = books.min { a, b in a.numberOfPages < b.numberOfPages }
let longestBook = books.max { a, b in a.numberOfPages < b.numberOfPages }

Or equivalently:

let shortestBook = books.min { $0.numberOfPages < $1.numberOfPages }
let longestBook = books.max { $0.numberOfPages < $1.numberOfPages }

Or equivalently:

let shortestBook = books.min {
  let pages1 = $0.numberOfPages
  let pages2 = $1.numberOfPages
  return pages1 < pages2
}

let longestBook = books.max {
  let pages1 = $0.numberOfPages
  let pages2 = $1.numberOfPages
  return pages1 < pages2
}

Whichever of the three ways above you do it, you should now be able to easily get the titles of the shortest and longest books like so:

shortestBook?.title // Dance of the Happy Shades
longestBook?.title // Battleborn

No for-loops and no array indices to keep track of. Yay!