Skip to content

Instantly share code, notes, and snippets.

@jparishy
Last active August 29, 2015 14:11
Show Gist options
  • Select an option

  • Save jparishy/9207d5398bef873fc221 to your computer and use it in GitHub Desktop.

Select an option

Save jparishy/9207d5398bef873fc221 to your computer and use it in GitHub Desktop.
map() & reduce() in Swift

Adopting map() & reduce() in Swift

Swift brings with it the ideologies of several programming paradigms. To Objective-C developers, the most obvious is likely to be a more functional style approach to solving problems. One of the fundamental aspects of functional programming is abstracting logic so that your code reads as a dictation of what it is doing, now how it is doing it. Unless performance becomes an issue, the implementation details of certain action are typically less important than the code being as declarative as possible.

One of the tools that makes this possible is the use of higher order functions, functions that take other functions and use them to perform transformations on a set of data. These include map(), reduce(), filter(), etc.

Swift includes these higher order functions and as you embrace the language itself it is also important that you begin to emprace them in order to make your code more idiomatic and easier to read and reason about.

The purpose of this article is show some common uses for these functions in your every day programming. We're going to fucus on map() and reduce() in particular, and explore some basic ideas as well as some more concrete examples. Let's dive in.

If you're a programmer it's very likely that at some point you've had to comma separate a list of strings.

Your first approach to solving this might include initializing an empty string, going through each item in the list, appending the current item to the string, and then adding the comma to separate it from the next item. It'd probably look something like this:

let coffees = [ "Cappuccino", "Latte", "Macchiato" ]
var commaSeparatedCoffees = ""

for coffee in coffees {
    commaSeparatedCoffees += coffee
    if coffee != coffees.last {
        commaSeparatedCoffees += ", "
    }
}

// Output: Cappucino, Latte, Macchiato

You knew what the output should be as soon as you read "comma separated" and yet, look how many steps it took me to explain it in words. Did any of that really expand on your understanding of what it is that we were doing in this code? No. Because the implementation details don't matter.

All we really want is to say that commaSeparatedCoffees is the result of some transformation on coffees and we want the code to be as succinct as that statement. This is a textbook use case for the reduce() function.

let coffees = [ "Cappuccino", "Latte", "Macchiato" ]
let commaSeparatedCoffes = coffees.reduce("") {
    wholeString, coffee in
    let maybeComma = (coffee == coffees.last) ? "" : ", "
    return "\(wholeString)\(coffee)\(maybeComma)"
}

// Output: Cappucino, Latte, Macchiato

And we can refactor this logic into a generic join() function like the ones available in other languages.

func join<T : Equatable>(objs: [T], separator: String) -> String {
    return objs.reduce("") {
        sum, obj in
        let maybeSeparator = (obj == objs.last) ? "" : separator
        return "\(sum)\(obj)\(maybeSeparator)"
    }
}

let fruits = [ "Apples", "Bananas", "Cherries" ]
let commaSeparatedFruits = join(fruits, ", ")

// Output: Apples, Bananas, Cherries

Similarly, we can use reduce() to add up a list of numbers. This comes in handy quite often.

let numbers = [Int](0..<10)
let total = numbers.reduce(0) {
    return $0 + $1
}

// Output: 45

or more succinctly:

let numbers = [Int](0..<10)
let total = numbers.reduce(0, +)

// Output: 45

However, usually it's not just a raw list of numbers. The numbers are often encapsulated in another data type. We can use map() to extract the numbers from the objects, then reduce() to add them together. For example, let's find the average age of a bunch of people.

struct Person {
    let name: String
    let age: Int
}
 
let people = [
    Person(name: "Katie",  age: 23),
    Person(name: "Bob",    age: 21),
    Person(name: "Rachel", age: 33),
    Person(name: "John",   age: 27),
    Person(name: "Megan",  age: 15)
]
 
let ages: [Int] = people.map { return $0.age }
let agesTotal   = ages.reduce(0) { return $0 + $1 }
let averageAge  = Double(agesTotal) / Double(ages.count)

// Output:
//  ages       = [23, 21, 33, 27, 15 ]
//  agesTotal  = 119
//  averageAge = 23.8

Sometimes, you have an array of array's that contain a particular data type, but you want to collapse or flatten that into a 1D array consisting of all the objects.

let arrayOfArrays = [
    [ 0, 1, 2 ],
    [ 3, 4, 5 ],
    [ 6, 7, 8 ]
]

let flattened: [Int] = arrayOfArrays.reduce([]) {
    res, ca in
    return res + ca
}

// Output:
//  flattened = [ 0, 1, 2, 3, 4, 5, 6, 7, 8 ]

Again we can refactor this into a generic method flatten that is implemented in terms of reduce()

func flatten<T>(a: [[T]]) -> [T] {
    return a.reduce([]) {
        res, ca in
        return res + ca
    }
}

let flattened = flatten(arrayOfArrays)

// Output:
//  flattened = [ 0, 1, 2, 3, 4, 5, 6, 7, 8 ]

Sometimes you want to find the unique values within a particular data set. For example, given a list of Users who each have a name and a list of things that they like, we want to obtain a list of all the unique likes between all the Users.

We can use a combination of map() and reduce() to achieve this.

  1. First we map() the users to their their likes, giving us an array of array's containg the likes.
  2. Next we flatten that list into a single array of likes using the flatten function we wrote earlier.
  3. Now we have our list, but it contains duplicated, so we ca apply reduce() to the list with an initial value of an empty set, and the reduce function will add each object to set.
  4. Because sets only contain unique objects, we end up with a unique list of likes.
struct User {
    let name: String
    let likes: [String]
}

let users = [
    User(name: "Julius", likes: [ "Programming", "Snow", "Whiskey", "Cats" ]),
    User(name: "Stephanie", likes: [ "Video Games", "Horseback Riding", "Cats", "Orange Juice" ]),
    User(name: "Bill", likes: [ "Pencils", "Summer", "Cats", "Horseback Riding" ]),
    User(name: "Melanie", likes: [ "Watches", "Cats", "Programming", "Writing" ])
]

let uniqueLikes: NSSet = flatten(users.map({ u in u.likes })).reduce(NSSet()) {
    set, like in
    return set.setByAddingObject(like)
}

// Output:
//  uniqueLikes = {(
//      Summer,
//      Cats,
//      "Orange Juice",
//      "Horseback Riding",
//       Programming,
//      Pencils,
//      Writing,
//      Watches,
//      Whiskey,
//      Snow,
//      "Video Games"
//  )}

Many times when implementing functionality such as form validation, you must combine the value of multiple booleans. reduce() is perfect for this.

let booleans = [
    false,
    false,
    true,
    false,
    true,
    true
]
 
let allTrue = booleans.reduce(true) {
    (sum, next) in
    return sum && next
}
 
let allFalse = booleans.reduce(true) {
    (sum, next) in
    return sum && !next
}
 
let anyTrue = booleans.reduce(false) {
    (sum, next) in
    return sum || next
}
 
let anyFalse = booleans.reduce(false) {
    (sum, next) in
    return sum || !next
}

// Output:
//  allTrue  = false
//  allFalse = false
//  anyTrue  = true
//  anyFalse = true

We can even refactor this logic into a more generic function combine() that operates in terms of reduce()

func combine(booleans: [Bool], initial: Bool, op: (Bool, @autoclosure () -> Bool) -> Bool, transform: Bool -> Bool) -> Bool {
    return booleans.reduce(initial) {
        sum, b in
        return op(sum, transform(b))
    }
}
func combine(booleans: [Bool], initial: Bool, op: (Bool, @autoclosure () -> Bool) -> Bool) -> Bool {
    return combine(booleans, initial, op, identity)
}

func identity(b: Bool) -> Bool {
    return b
}

func inverse(b: Bool) -> Bool {
    return !b
}

let allTrue  = combine(booleans, true, &&)
let allFalse = combine(booleans, true, &&, inverse)
let anyTrue  = combine(booleans, false, ||)
let anyFalse = combine(booleans, false, ||, inverse)

// Output:
//  allTrue  = false
//  allFalse = false
//  anyTrue  = true
//  anyFalse = true

In practice, you may have a list of options in your app and the state of each is represented by an OptionState object.

Given a list of OptionState objects, we can perform validation on the interactions with the list of options.

struct OptionState {
    let title: String
    let selected: Bool
}
 
let optionStates = [
    OptionState(title: "Objective-C", selected: true),
    OptionState(title: "Swift",       selected: true),
    OptionState(title: "Haskell",     selected: false),
    OptionState(title: "Ruby",        selected: true)
]

For example, are they all selected?

let allSelected = optionStates.reduce(true) {
    return $0 && $1.selected
}

// Output:
//  allSelected = false

Or are any of them selected? Here we apply map() first so the reduce() function works with only Bools and not OptionState objects like in the previous example.

let anySelected = optionStates.map({ os in os.selected }).reduce(false) {
    return $0 || $1
}

// Output:
//  anySelected = true

From reading these examples, it should become more clear how using these functions can make your code easier to reason about. You begin declaring what you're doing and not how you're doing it. As you begin to write more and more code in this manner, additional uses for map() and reduce() will come organically. Give it a shot!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment