Sequence Hacking in Swift (I): map, flatMap, sort, filter, reduce - Digital Leaves
1911
post-template-default,single,single-post,postid-1911,single-format-standard,ajax_fade,page_not_loaded,,select-theme-ver-3.8,wpb-js-composer js-comp-ver-5.1.1,vc_responsive

Sequence Hacking in Swift (I): map, flatMap, sort, filter, reduce

Hi there! This is the first of a series of posts focused on sequences in Swift, aimed at turn you into a sequence ninja in Swift, able to modify and extend sequences to accomplish really complex and awesome stuff with just a few commands. This first post is aimed at teaching the basics of sequence manipulation, specially focused in the cool operations map, flatMap, sort, filter and reduce.

About Sequences

Sequence is the most basic type in Swift for defining an aggregation of elements that distribute sequentially in a row. All common collection types inherit from it, including Array, Set, Dictionary, and all other swift Collections. In its core, a Sequence is nothing more than an ordered queue of items that can be iterated from beginning to end, using an Iterator. The Sequence is absolutely agnostic on the types of elements it contains, offering only the common functions to retrieve, iterate and traverse over its elements.

Sequence has a number of child subtypes, like Collection, that is also the foundation for more well-known types such as Set, Array, Dictionary, etc. A Collection is nothing more than an indexable sequence, and adding Hashable and other protocols result in more useful types like Array, that all iOS developers use every day.

However, the simplicity of Sequence hides a very capable and feature rich type which offers some pretty cool methods for it and all of its descendants. Before getting into hacking with sequences, let’s have a look at these basic methods and operations, and how we can use them to do some pretty interesting stuff:

Our sample environment: People who like books

Our sample application will contain a series of instances of the class People, containing three basic properties: the name of the person, its age, and an array of the favorite books for that person:

class Person: CustomStringConvertible {
    var name: String
    var age: Int
    var favoriteBooks: [String]

    init(name: String, age: Int, favoriteBooks: [String]) {
        self.name = name
        self.age = age
        self.favoriteBooks = favoriteBooks
    }
    var description: String { return "\(name), aged \(age), \(favoriteBooks.count) fav. books" }
}

let anne = Person(name: "Anne Smith", age: 23, favoriteBooks: ["Harry Potter", "Twilight", "New Moon", "Eclipse", "Breaking Dawn"])

let john = Person(name: "John Smith", age: 47, favoriteBooks: ["Dune", "Prelude to Foundation", "Forward the Foundation", "Foundation", "Foundation and Empire", "Second Foundation", "The Edges of the Foundation"])

let joseph = Person(name: "Joseph Campbell", age: 35, favoriteBooks: ["Dune", "The Fellowship of the Ring", "The Two Towers", "The Return of the King", "Harry Potter", "The Hobbit"])

let people = [anne, john, joseph]

Pretty simple, huh? We have this “people” array that, as you just learned, is also a Sequence, to illustrate how we can apply the different operations to extract, sort, filter or group these people depending on our intentions. Let’s start with “map”!.

Map

Map applies a mapping function to the elements of the sequence, resulting in another sequence. Its mathematical definition ensures that the resulting sequence will have the exact same elements that the input sequence. Maps are usually used to extract properties of a group of objects of a common class, or to perform a common operation on all the elements of a sequence. The mapping syntax is:

let outputSequence = inputSequence.map { (element) -> ResultType in
   return something related to element
}

In our case, let’s see how to get an array with all the names of the people in our array:

let names = people.map { (person) -> String in
   return person.name
}

Pretty simple, right? This syntax is pretty intuitive, but there is another, simplified syntax that uses $0 instead of element, and allows us to build very concise instructions (specially if chaining several sequence operations):

let names2 = people.map({ $0.name })

Note how the “return” clause has been removed too. This names2 array is exactly the same as the initial names array. Now, if we try to get the list of all favorite books from the people in the array:

let books1 = people.map({ $0.favoriteBooks })
// books1 = [["Harry Potter", "Twilight", "New Moon", "Eclipse", "Breaking Dawn"], ["Dune", "Prelude to Foundation", "Forward the Foundation", "Foundation", "Foundation and Empire", "Second Foundation", "The Edges of the Foundation"], ["Dune", "The Fellowship of the Ring", "The Two Towers", "The Return of the King", "Harry Potter", "The Hobbit"]]

Notice how we are getting an array of arrays. This is not what we probably want. This is because map is returning the exact type that corresponds to the mapping transformation. However, we have a pretty useful operation for this exact case, getting all the books in the same array, called flatMap.

FlatMap

FlatMap works like map, except that it “flattens” the output, to combine all sequences or collections in a single one. Let’s see an example with the previous example:

let books2 = people.flatMap({ return $0.favoriteBooks })
// books2 = ["Harry Potter", "Twilight", "New Moon", "Eclipse", "Breaking Dawn", "Dune", "Prelude to Foundation", "Forward the Foundation", "Foundation", "Foundation and Empire", "Second Foundation", "The Edges of the Foundation", "Dune", "The Fellowship of the Ring", "The Two Towers", "The Return of the King", "Harry Potter", "The Hobbit"]

Now this is exactly what we wanted. Notice how we are getting duplicates, because some of the books where in the favorite list of several of the person instances of the array. We’ll see how to remove duplicates later, but next, let’s talk about another cool operation, sort.

Sort(ed)

You might have used sorted before without knowing you were accessing a generic sequence operation. Its syntax can be expressed as:

let outputSequence = inputSequence.sorted { (element1, element2) -> Bool in
   return true if element1 goes before element2
}

As in the case of map and flatMap, we have a reduced syntax, in this case using $0 for element1 and $1 for element2. Let’s see some examples of both:

First, let’s sort our people by age:

let sortedByAge = people.sorted { (person1, person2) -> Bool in
   return person1.age < person2.age
}
// sortedByAge = [Anne Smith, aged 23, 5 fav. books, Joseph Campbell, aged 35, 6 fav. books, John Smith, aged 47, 7 fav. books]

Now, let’s sort them by those who read more books:

let sortedByNumberOfBooks = people.sorted(by: { $0.favoriteBooks.count > $1.favoriteBooks.count })
// sortedByNumberOfBooks = [John Smith, aged 47, 7 fav. books, Joseph Campbell, aged 35, 6 fav. books, Anne Smith, aged 23, 5 fav. books]

We can build an extension on Sequence making use of the Comparable protocol to build a custom sort function:

extension Sequence where Iterator.Element: Comparable {
  func sortMeIAmASequence() -> [Iterator.Element] {
    return sorted { $0 < $1 }
  }
}

let numbers = [23.23478, -2.32784, 34.328, 33.28347]
let sortedNumbers = numbers.sortMeIAmASequence()
// sortedNumbers = [-2.3278400000000001, 23.234780000000001, 33.283470000000001, 34.328000000000003]

We’ll talk in depth about extending sequences in following episodes of this series. Next, let’s talk about the always sexy filter operation.

Filter

Filter is a very powerful way of getting just the information that we want from a sequence. Let’s say that we want to get the people older than 30, it’s pretty simple using a one-line filter:

let notSoYoung = people.filter({ $0.age > 30 })
// notSoYoung = [John Smith, aged 47, 7 fav. books, Joseph Campbell, aged 35, 6 fav. books]

Next, let’s talk about reduce.

Reduce

Reduce is one of the less intuitive but more useful sequence operations. It allows us to extract one single value from a sequence, by performing a series of operations in the sequence’s elements. Its syntax is the following:

let outputValue = inputSequence.reduce(initialValue) { (currentValue, nextElement) -> T in
   return a value that can be computed in the next element.
}

Here, T is the resulting type for the operation, like Int or Double. We must provide an initial value (initialValue) for the iteration, and return on every step the accumulative result of applying the reduction to the current value with the next element. Reduce is easier to understand with an example in mind, so let’s see how we can sum of an array of doubles (the result type will, thus, be Double):

let numbers = [23.23478, -2.32784, 34.328, 33.28347]
let sum = numbers.reduce(0) { (result, next) -> Double in
   return result + next
}
// sum = 88.51841

This is really useful for calculating averages in just one line. The operation that we apply in the closure can be specified as a function that takes the input parameters and returns the desired result type. In our case, as we are just adding elements, we can replace it by the addition operation (+):

let average = numbers.reduce(0, +) / Double(numbers.count)
// average = 22.1296025

Now, we can use again a Sequence extension to get an array of the unique favorite books for the people in our array. Remember that we had those in an array called books2, including the duplicated elements? Let’s define a “unique()” extension for Sequence to get rid of the duplicates:

extension Sequence where Iterator.Element: Equatable {
  func unique() -> [Iterator.Element] {
    return reduce([], { collection, element in collection.contains(element) ? collection : collection + [element] })
  }
}
let uniqueBooks = books2.unique()
// uniqueBooks = ["Harry Potter", "Twilight", "New Moon", "Eclipse", "Breaking Dawn", "Dune", "Prelude to Foundation", "Forward the Foundation", "Foundation", "Foundation and Empire", "Second Foundation", "The Edges of the Foundation", "The Fellowship of the Ring", "The Two Towers", "The Return of the King", "The Hobbit"]

Chaining Operations

Now the most powerful and cool aspect of the Sequence operations is that they can be easily chained together to get some one-line awesome results. As most of these operations (with the exception of reduce) return a Sequence, we can chain them to get the average length for the titles of the books of all the people that are older than 30 in our array:

let averageTitleLengthsFromNotSoYoungPeople = Double(people.filter({ $0.age > 30 }).flatMap({ $0.favoriteBooks }).map({ $0.characters.count }).reduce(0, +)) / Double(people.filter({ $0.age > 30 }).flatMap({ $0.favoriteBooks }).count)
// Average title length: 16.1538461538462

Not specially useful or probably efficient, but this is just an example to show you the power of chaining several operations together.

Where to go from here

In this episode of the “Sequence Hacking in Swift” series, we explored the Sequence type and showed some real example uses of its most useful operations: map, flatMap, sorted, filter, and reduce. In the next chapter, we are going to learn how to build extensions to Sequence (and derived types) that are really useful to add quick complex capabilities to our Swift code.

Which are your favorite uses of these operations? Do you have a killing combo for your apps? Let us know in the comments below!

2 Comments

Post a Comment

Before you continue...

Hi there! I created the Digital Tips List, the newsletter for Swift and iOS developers, to share my knowledge with you.


It features exclusive weekly tutorials on Swift and iOS development, Swift code snippets and the Digital Tip of the week.


Besides, If you join the list you'll receive the eBook: "Your First iOS App", that will teach you how to build your first iOS App in less than an hour with no prior Swift or iOS knowledge.

I hate spam, and I promise I'll keep your email address safe.