You are currently viewing 5 Complex Algorithms Simplified Using Swift’s Higher-Order Functions

5 Complex Algorithms Simplified Using Swift’s Higher-Order Functions

As a developer, very often we need to deal with complex algorithms that take hours or even days to develop. Thanks to Swift’s higher-order functions such as map, reduce, filter, etc, some of those complex algorithms can now be easily solved by just a few lines of code.

In this article, I would like to show you 5 algorithms that used to be difficult to implement are now extremely easy to achieve, thanks to the higher-order functions in Swift.

Throughout this article, I will be using the following students array as the model object, so that you can have a better picture of how these higher-order functions work.

// The model object of upcoming examples
let students = [
    Student(id: "001", name: "Jessica", gender: .female, age: 20),
    Student(id: "002", name: "James", gender: .male, age: 25),
    Student(id: "003", name: "Mary", gender: .female, age: 19),
    Student(id: "004", name: "Edwin", gender: .male, age: 27),
    Student(id: "005", name: "Stacy", gender: .female, age: 18),
    Student(id: "006", name: "Emma", gender: .female, age: 22),
]

enum Gender {
    case male
    case female
}

struct Student {
    let id: String
    let name: String
    let gender: Gender
    let age: Int
}

With that being said, let’s get right into it!


Grouping Array Elements by Criteria

Let’s say we want to group the students by the first alphabet of their name. Traditionally, we will have to manually loop through each element in the array and group them accordingly.

Now, with the help of the Dictionary(grouping:by:) initializer, we can achieve that without using a for-in loop. Here’s how:

let groupByFirstLetter = Dictionary(grouping: students) { student in
    return student.name.first!
}

/*
 Output:
 [
    "E": [Edwin, Emma],
    "M": [Mary],
    "J": [Jessica, James],
    "S": [Stacy]
 ]
 */

As you can see from the above sample code, the initializer will generate a dictionary of type [KeyType: Student]. This is especially useful if we want to group the student by criteria and show them in a multi sections table view.

We can even further simplify the above code by using the shorthand argument names or key path syntax in Swift:

// Using shorthand argument names
let groupByFirstLetter = Dictionary(grouping: students, by: { $0.name.first! })

// Using key path syntax
let groupByFirstLetter = Dictionary(grouping: students, by: \.name.first!)

Note:

Want to know how to group array elements by custom object? check out my previous article “Grouping Array Elements With Dictionary in Swift“.


Counting Occurrence of Array Elements

Counting the total number of elements in an array is easy, but what if we want to count the occurrence of elements based on certain criteria? For example, let’s say we want to know how many male and female students are there in the students array.

One way to go about this is by using the Dictionary(grouping:by:) initializer we just saw:

let groupByGender = Dictionary(grouping: students, by: \.gender)

let femaleCount = groupByGender[.female]!.count // Output: 4
let maleCount = groupByGender[.male]!.count     // Output: 2

The above approach might give us the desired result, however, it does have some memory overhead. As you can see, the initializer will generate arrays of male and female students that we don’t really need, what we need is just the occurrence of the male and female students.

To overcome the memory overhead, we can leverage the array’s reduce(into:) function. Let’s take a look at the following sample code:

let genderCount = students.reduce(into: [Gender: Int]()) { result, student in
    
    guard var count = result[student.gender] else {
        // Set initial value to `result`
        result[student.gender] = 1
        return
    }
    
    // Increase counter by 1
    count += 1
    result[student.gender] = count
}

let femaleCount = genderCount[.female]! // Output: 4
let maleCount = genderCount[.male]!     // Output: 2

What the above sample code does is reduce the students array into a dictionary of type [Gender: Int]. Within the closure, we cumulatively populate the final dictionary by counting the occurrence of male and female students.

Now that you have understood how to count the occurrence using the reduce(into:) function, let’s further simplify the sample code by giving the result dictionary a default value of 0 like so:

let genderCount = students.reduce(into: [Gender: Int]()) { result, student in
    result[student.gender, default: 0] += 1
}

let femaleCount = genderCount[.female]! // Output: 4
let maleCount = genderCount[.male]!     // Output: 2

With that, we have avoided the memory overhead while keeping our code simple and clean.


Getting the Sum of an Array

Next up, l would like to show you how to get the sum of an array with only 1 single line of code. Let’s say we want to get the sum of the students’ age. For that, we can use the array’s reduce(_:_:) function like so:

let sum = students.reduce(0) { result, student in
    return result + student.age
}
// Output: 131

As you might have guessed, we can further improve the above sample code by using the shorthand argument names:

let sum = students.reduce(0, { $0 + $1.age })

For cases where the array element type is type that support the addition operator (+), we can simplify it even further by omitting the shorthand argument names:

let sum1 = [2, 3, 4].reduce(0, +)          // Output: 9
let sum2 = [5.5, 10.7, 9.43].reduce(0, +)  // Output: 44.435
let sum3 = ["a","b","c"].reduce("", +)     // Output: "abc"

Pretty cool isn’t it?


Accessing Array Elements by ID

When dealing with arrays, one of the most common operations we need to perform is to find a specific array element using an object ID. The most straightforward way to go about this is to either use a for-in loop or the array’s filter(_:) function to loop through each array element.

Both of these approaches are good enough for most cases. However, both of them have O(n) time complexity, meaning when the array gets bigger, they will take more time to find the specific elements. For apps that strive for speed and responsiveness, these approaches will definitely create a performance bottleneck.

In order to make this an operation with O(1) complexity, we can transform the students array into a dictionary where the key is the student ID and the value is the Student object.

For that, we will first leverage the array’s map(_:) function to transform the array into an array of tuples with student ID and Student object. After that, we will transform the tuple array into a dictionary using the Dictionary(uniqueKeysWithValues:) initializer.

// Transform [Student] --> [(String, Student)]
let studentsTuple = students.map { ($0.id, $0) }

// Transform [(String, Student)] --> [String: Student]
let studentsDictionary = Dictionary(uniqueKeysWithValues: studentsTuple)

// Read from dictionary (this is O(1) operation)
let emma = studentsDictionary["006"]!

Do note that the process of transforming the array to a dictionary is still an O(n) operation. However, we only need to do it once. Once the dictionary is ready, any read operation performed on the dictionary is an O(1) operation.


Getting a Number of Random Elements From An Array

This last example does not involve any higher-order function, but I think it is still worth sharing. Getting a number of random elements from an array used to be a difficult algorithm to implement because we need to handle various kinds of edge cases.

Now with the help of the shuffled() and prefix(_:) functions in Swift array, this operation has become extremely easy to achieve.

Here’s how to randomly pick 3 students from the students array:

// Randomize all elements within the array
let randomized = students.shuffled()

// Get the first 3 elements in the array
let selected = randomized.prefix(3)

One good thing about this approach is that it won’t trigger index out of range exception even if the number of elements we are trying to get is more than the array’s total elements.


Wrapping up

All the examples shown above can definitely be solved by using a traditional for-in loop. However, that will require us to manually handle various kinds of edge cases, therefore it is extremely error-prone.

By using the higher-order functions, we can highly reduce the complexity of our code, thus making it less error-prone. Most importantly, it makes our code much easier to maintain.

What do you think about using Swift’s higher-order functions to reduce code complexity? Do you have any other examples that you would like to share? Feel free to reach out to me on Twitter.

Thanks for reading. 🧑🏻‍💻


Further Readings


👋🏻 Hey!

While you’re still here, why not check out some of my favorite Mac tools on Setapp? They will definitely help improve your day-to-day productivity. Additionally, doing so will also help support my work.

  • Bartender: Superpower your menu bar and take full control over your menu bar items.
  • CleanShot X: The best screen capture app I’ve ever used.
  • PixelSnap: Measure on-screen elements with ease and precision.
  • iStat Menus: Track CPU, GPU, sensors, and more, all in one convenient tool.