Mastering Higher-Order Functions in Swift

Unlock the Power of Functional Programming for Efficient Data Manipulation

Vikram Kumar
16 min readOct 19, 2023

Introduction:

Swift, Apple’s versatile and powerful programming language, is known for its modern syntax, robust standard library, and a wide array of features that empower developers to create efficient and elegant solutions. Among these features, higher-order functions stand out as a vital component of functional programming, enabling developers to manipulate collections of data in a concise and expressive manner. In this comprehensive guide, we will explore the world of higher-order functions in Swift and provide you with a hands-on understanding of how they work, and how they can be used to streamline your code.

Higher-order functions in Swift allow you to treat functions as first-class citizens, empowering you to pass them as arguments or return them as results. By mastering these functions, you’ll be able to write code that is not only more readable and maintainable but also more efficient.

In the following sections, we will delve into essential higher-order functions such as map, filter, reduce, and more, providing clear coding examples for each. You'll discover how to chain these functions together for complex data transformations, and we'll even cover the creation of custom higher-order functions to enhance code reusability. Whether you're new to Swift or looking to elevate your programming skills, this guide will equip you with the knowledge and tools to harness the full potential of higher-order functions in Swift. Let's embark on this journey to become a master of functional programming in Swift.

Photo by ThisisEngineering RAEng on Unsplash

Understanding Higher-Order Functions

Higher-order functions are a concept borrowed from functional programming. They allow you to treat functions as first-class citizens in your code, enabling you to perform operations on functions, pass them as arguments, or return them as results. In Swift, the primary higher-order functions include map, filter, reduce, and flatMap, among others.

Let’s dive into these functions with examples to better understand how they work.

1. map

The map function transforms each element of a collection into a new element, applying a specified transformation function. It creates a new collection with the transformed elements.

Example 1: Squaring Numbers

Suppose you have an array of integers and want to create a new array that contains the square of each number. Here’s how you can use map to achieve this:

let numbers = [1, 2, 3, 4, 5]
let squaredNumbers = numbers.map { $0 * $0 }

print(squaredNumbers) // [1, 4, 9, 16, 25]

In this example, the map function applies the closure { $0 * $0 } to each element in the numbers array, resulting in a new array squaredNumbers containing the squares of the original numbers.

Example 2: Capitalizing Strings

Let’s say you have an array of strings and you want to create a new array with all the strings capitalized. Here’s how you can use map to achieve this:

let words = ["apple", "banana", "cherry"]
let capitalizedWords = words.map { $0.capitalized }

print(capitalizedWords) // ["Apple", "Banana", "Cherry"]

In this case, the map function applies the capitalized method to each string in the words array, resulting in a new array capitalizedWords with capitalized strings.

Example 3: Mapping to a Different Type

map is not limited to transforming elements within the same type. You can map elements to a different type if your transformation closure returns a different type. For example, let's convert an array of integers into an array of strings:

let numbers = [1, 2, 3, 4, 5]
let numberStrings = numbers.map { String($0) }

print(numberStrings) // ["1", "2", "3", "4", "5"]

Here, the map function applies the closure { String($0) } to each integer, converting them to strings.

Example 4: Mapping an Array of Custom Objects

You can also use map to transform an array of custom objects. Suppose you have a Person struct, and you want to extract the names of individuals from an array of Person objects:

struct Person {
let name: String
let age: Int
}

let people = [
Person(name: "Alice", age: 30),
Person(name: "Bob", age: 25),
Person(name: "Charlie", age: 35)
]

let names = people.map { $0.name }

print(names) // ["Alice", "Bob", "Charlie"]

In this case, map extracts the name property from each Person object, resulting in a new array of names.

The map function takes a collection and a closure as input. The closure defines the transformation that should be applied to each element of the collection. As the map function iterates through the collection, it applies the closure to each element and generates a new collection that contains the transformed elements.

The map function is a versatile tool for transforming elements in Swift collections. It simplifies the process of applying a transformation to every element in an array and creating a new array with the transformed values. By mastering map, you can make your code more concise, expressive, and efficient.

2. filter

In the world of Swift, the filter function is a vital higher-order function that allows you to create a new collection containing only the elements that satisfy a specified condition. This feature is invaluable when you need to extract elements that meet certain criteria from a collection.

Example 1: Filtering Even Numbers

Suppose you have an array of integers and want to create a new array containing only the even numbers. Here’s how you can use filter to achieve this:

let numbers = [1, 2, 3, 4, 5, 6]
let evenNumbers = numbers.filter { $0 % 2 == 0 }

print(evenNumbers) // [2, 4, 6]

In this example, the filter function applies the closure { $0 % 2 == 0 } to each element in the numbers array. Elements that meet the condition (i.e., those that are even) are included in the evenNumbers array.

Example 2: Filtering Strings by Length

Suppose you have an array of strings and want to extract all the strings with a length greater than 5 characters. Here’s how you can use filter for this purpose:

let words = ["apple", "banana", "cherry", "date", "elderberry"]
let longWords = words.filter { $0.count > 5 }

print(longWords) // ["banana", "cherry", "elderberry"]

The filter function applies the closure { $0.count > 5 } to each string in the words array, retaining strings with a length greater than 5 characters in the longWords array.

Example 3: Filtering Custom Objects

filter is not limited to basic data types. You can also use it to filter an array of custom objects. Suppose you have an array of Person objects, and you want to extract individuals who are older than 30:

struct Person {
let name: String
let age: Int
}

let people = [
Person(name: "Alice", age: 35),
Person(name: "Bob", age: 28),
Person(name: "Charlie", age: 42)
]

let olderThan30 = people.filter { $0.age > 30 }

print(olderThan30) // [Person(name: "Alice", age: 35), Person(name: "Charlie", age: 42)]

In this case, the filter function applies the closure { $0.age > 30 } to each Person object, retaining only those individuals older than 30 in the olderThan30 array.

Example 4: Filtering with Multiple Conditions

You can also filter a collection using multiple conditions combined with logical operators. Let’s filter an array of numbers to include only the ones that are both even and greater than 4:

let numbers = [2, 4, 6, 7, 8, 9]
let filteredNumbers = numbers.filter { $0 % 2 == 0 && $0 > 4 }

print(filteredNumbers) // [6, 8]

The filter function allows you to apply multiple conditions within the closure to extract elements that meet all the specified criteria.

The filter function takes a collection and a closure as input. The closure is used to define the condition that an element must meet to be included in the resulting collection. Elements for which the condition evaluates to true are retained, while those for which it evaluates to false are excluded.

The filter function is a versatile tool in Swift that simplifies the process of extracting elements from a collection based on specific conditions. Whether you're working with numbers, strings, or custom objects, filter can help you keep only the data that is relevant to your application. This results in cleaner, more efficient, and more focused code, ultimately making your Swift programming experience more enjoyable and productive.

3. reduce

The reduce function is a powerful higher-order function that enables you to combine all the elements of a collection into a single value using a specified combining function. This function is incredibly useful when you need to perform aggregation, summation, or any other operation that involves accumulating values.

The reduce function takes an initial value, also known as an accumulator, and a closure as input. The closure specifies how each element of the collection should be combined with the accumulator. As the reduce function processes each element in the collection, it accumulates the results based on the rules defined in the closure. The final output is a single value.

Example 1: Summing an Array of Numbers

Consider you have an array of integers, and you want to calculate the sum of all the elements in the array. You can use the reduce function for this task:

let numbers = [1, 2, 3, 4, 5]
let sum = numbers.reduce(0) { (accumulator, number) in
return accumulator + number
}

print(sum) // 15

In this example, the reduce function starts with an initial value of 0 and iterates through the numbers array, adding each number to the accumulator. The result is the sum of all the numbers.

Example 2: Combining Strings

Suppose you have an array of strings and want to concatenate them into a single string. Here’s how you can achieve this using reduce:

let words = ["Hello", ", ", "world", "!"]
let combinedString = words.reduce("") { (accumulator, word) in
return accumulator + word
}

print(combinedString) // "Hello, world!"

In this case, the reduce function starts with an empty string as the initial value. It iterates through the words array, combining each word with the accumulator, resulting in a single concatenated string.

The reduce function is an indispensable tool for aggregating, combining, and accumulating values in Swift. Whether you're working with numbers, strings, or custom objects, reduce allows you to elegantly summarize data. By mastering this function, you can write more efficient and expressive code while performing complex data transformations and aggregations.

4. flatMap

In Swift, the flatMap function is a powerful higher-order function that combines the capabilities of both map and joined. It allows you to transform and flatten a collection of optionals or nested collections into a single, non-optional collection. Understanding and mastering flatMap is essential for working with complex data structures and optional values in Swift.

Example 1: Flattening Nested Arrays

Suppose you have an array of arrays, and you want to flatten them into a single array. Here’s how you can use flatMap to achieve this:

let nestedArrays = [[1, 2, 3], [4, 5], [6, 7, 8]]
let flattenedArray = nestedArrays.flatMap { $0 }

print(flattenedArray) // [1, 2, 3, 4, 5, 6, 7, 8]

In this example, the flatMap function iterates through each sub-array within the nestedArrays array, extracting and flattening the elements into a single array flattenedArray.

Example 2: Transforming and Filtering Using flatMap

You can also use flatMap to transform and filter elements within nested arrays. Suppose you want to double all even numbers and discard the odd numbers in the nested arrays:

let nestedArrays = [[1, 2, 3], [4, 5], [6, 7, 8]]
let modifiedArray = nestedArrays.flatMap { subArray in
return subArray.compactMap { number in
return number % 2 == 0 ? number * 2 : nil
}
}

print(modifiedArray) // [4, 8, 8, 16]

In this case, flatMap applies the transformation and filtering operations to each element within the nested arrays, and the result is a single flattened array with the modified values.

Example 3: Handling Optionals

flatMap is particularly useful when dealing with optionals. It allows you to safely unwrap and process optional values within a collection, discarding any nil values in the process. Suppose you have an array of optional integers and want to extract and process only the non-nil values:

let optionalNumbers: [Int?] = [1, 2, nil, 4, nil, 6]
let nonNilNumbers = optionalNumbers.flatMap { $0 }

print(nonNilNumbers) // [1, 2, 4, 6]

Here, flatMap safely unwraps the optional values, discarding the nil elements and returning only the non-nil integers in a single array.

The flatMap function is designed to address scenarios where you have a collection of nested data structures, such as arrays within arrays or optionals within optionals. It takes a collection and a closure as input. The closure defines the transformation that should be applied to each element of the collection, but with flatMap, you are expected to return an optional or another collection. flatMap then combines the results from all elements into a single, non-optional collection.

The flatMap function is an essential tool for working with nested collections and optionals in Swift. It simplifies the process of transforming and flattening complex data structures, making your code more concise and expressive. Whether you're handling arrays within arrays, optionals, or any nested data, mastering flatMap is key to improving your Swift programming skills.

5. compactMap

The compactMap function is a versatile higher-order function that enables you to filter and transform a collection of optionals, discarding any nil values in the process. This function is particularly useful when dealing with collections that may contain optional elements, such as arrays of optionals.

Example 1: Filtering nil Values

Suppose you have an array of optional integers, and you want to create a new array that contains only the non-nil values. Here’s how you can use compactMap to achieve this:

let optionalNumbers: [Int?] = [1, 2, nil, 4, nil, 6]
let nonNilNumbers = optionalNumbers.compactMap { $0 }

print(nonNilNumbers) // [1, 2, 4, 6]

In this example, the compactMap function iterates through each element of the optionalNumbers array, discarding any nil values and producing a new array nonNilNumbers with only the non-nil integers.

Example 2: Transforming and Filtering Optionals

You can also use compactMap to both transform and filter optional values. Suppose you want to double each optional integer in an array and filter out any nil values:

let optionalNumbers: [Int?] = [1, 2, nil, 4, nil, 6]
let doubledNumbers = optionalNumbers.compactMap { optionalNumber in
return optionalNumber.map { $0 * 2 }
}

print(doubledNumbers) // [2, 4, 8, 12]

In this case, the compactMap function applies the transformation { $0 * 2 } to each non-nil integer, doubling its value, and filters out any nil values in the process.

Example 3: Parsing Strings to Ints

compactMap is also valuable for parsing and converting strings to integer values. Suppose you have an array of strings that may represent integers, and you want to create a new array of integer values while ignoring any invalid or non-integer strings:

let stringNumbers = ["1", "2", "3", "four", "5", "six"]
let integers = stringNumbers.compactMap { Int($0) }

print(integers) // [1, 2, 3, 5]

In this example, the compactMap function applies the Int($0) conversion to each string element, returning the integer if it's a valid integer representation and filtering out any invalid entries.

The compactMap function is designed for scenarios where you have a collection of optionals and you want to filter out the nil values while transforming the non-nil values. It takes a collection and a closure as input. The closure defines the transformation that should be applied to each element of the collection. The key difference is that with compactMap, the closure is expected to return an optional value. The compactMap function then filters out the nil results, producing a new collection with only the non-nil values.

The compactMap function is a versatile tool for working with collections of optionals in Swift. It simplifies the process of filtering out nil values while transforming and handling the non-nil values. Whether you're working with arrays of optionals or handling optional values within collections, mastering compactMap is crucial for writing more concise, expressive, and robust code.

6. sorted

The sorted function is a powerful higher-order function that allows you to order the elements of a collection based on a specified sorting function. This function is essential for arranging data in a particular order, whether it's ascending or descending.

Example 1: Sorting Numbers in Ascending Order

Suppose you have an array of numbers, and you want to arrange them in ascending order. Here’s how you can use sorted to achieve this:

let numbers = [5, 2, 8, 1, 7]
let ascendingOrder = numbers.sorted { $0 < $1 }

print(ascendingOrder) // [1, 2, 5, 7, 8]

In this example, the sorted function applies the closure { $0 < $1 } to compare each pair of elements in the numbers array. The result is a new array, ascendingOrder, with the elements arranged in ascending order.

Example 2: Sorting Strings in Descending Order

Let’s say you have an array of strings, and you want to arrange them in descending order based on their character count. Here’s how you can use sorted to achieve this:

let words = ["apple", "banana", "cherry", "date", "elderberry"]
let descendingOrder = words.sorted { $0.count > $1.count }

print(descendingOrder) // ["elderberry", "banana", "cherry", "apple", "date"]

In this case, the sorted function applies the closure { $0.count > $1.count } to compare the lengths of each pair of strings. The result is a new array, descendingOrder, with the strings arranged in descending order based on their character count.

Example 3: Sorting Custom Objects

You can use sorted to order collections of custom objects based on specific criteria. Suppose you have an array of Person objects, and you want to arrange them in ascending order based on their ages:

struct Person {
let name: String
let age: Int
}

let people = [
Person(name: "Alice", age: 30),
Person(name: "Bob", age: 25),
Person(name: "Charlie", age: 35)
]

let sortedPeople = people.sorted { $0.age < $1.age }

print(sortedPeople)

In this example, the sorted function applies the closure { $0.age < $1.age } to compare the ages of each pair of Person objects. The result is a new array, sortedPeople, with the individuals arranged in ascending order of their ages.

The sorted function takes a collection and a closure as input. The closure defines the sorting criteria for the elements within the collection. The sorted function then arranges the elements according to these criteria, producing a new collection with the elements ordered as specified.

The sorted function is a versatile tool for arranging data in Swift collections based on specified criteria. Whether you're working with numbers, strings, custom objects, or need to sort by multiple criteria, mastering sorted is essential for organizing your data efficiently and effectively.

7. forEach

The forEach method is a versatile higher-order function that allows you to iterate over the elements of a collection and apply a specified operation to each element. It's a powerful tool for performing tasks on every item in a collection, making your code more concise and expressive.

Example 1: Printing Array Elements

Suppose you have an array of integers, and you want to print each element. Here’s how you can use forEach to achieve this:

let numbers = [1, 2, 3, 4, 5]

numbers.forEach { number in
print(number)
}

In this example, the forEach method iterates through each element in the numbers array and applies the provided closure, which prints each element to the console.

Example 2: Updating Array Elements

Let’s say you have an array of prices, and you want to increase each price by a fixed amount. Here’s how you can use forEach to update the elements in the array:

var prices = [10.0, 15.0, 20.0, 25.0]

let priceIncrease = 5.0

prices.forEach { price in
price += priceIncrease
}

print(prices) // [10.0, 15.0, 20.0, 25.0]

In this example, the forEach method iterates through the prices array, but notice that it won't modify the original array elements. To update the array, you should use a for loop or the map method.

Example 3: Custom Operations on Elements

forEach is not limited to basic operations like printing or updating values. You can perform any custom operation on each element. Suppose you have an array of strings, and you want to create a new array with the character count of each string:

let words = ["apple", "banana", "cherry", "date"]
var characterCounts: [Int] = []

words.forEach { word in
let count = word.count
characterCounts.append(count)
}

print(characterCounts) // [5, 6, 6, 4]

In this example, the forEach method is used to iterate through the words array and calculate the character count for each string, then append the counts to a new array, characterCounts.

Example 4: Applying Operations on a Dictionary

You can use forEach with dictionaries as well. Suppose you have a dictionary of names and ages, and you want to print each person's name and age:

let people = ["Alice": 30, "Bob": 25, "Charlie": 35]

people.forEach { name, age in
print("\(name) is \(age) years old.")
}

In this example, the forEach method iterates through the dictionary, extracting both the name and age, and then prints this information for each person.

The forEach method is used for iterating through a collection, such as an array or a dictionary, and applying a provided closure to each element. It doesn't return a new collection but allows you to perform actions on each item, making it ideal for tasks like printing values, updating elements, or performing any other operation on each item.

The forEach method is a versatile tool for iterating through collections and applying operations to each element. Whether you're working with arrays, dictionaries, or any other collection, mastering forEach allows you to make your code more concise and expressive when you need to perform actions on each item in the collection.

Chaining Higher-Order Functions:

Chaining higher-order functions is all about taking the output of one function and using it as the input for another. It creates a pipeline of transformations, allowing you to express data manipulation operations in a clear and concise manner. To successfully chain these functions, you need to understand the output and input types of each function, as they should align correctly in the sequence.

Example: Filtering and Mapping

Suppose you have an array of numbers, and you want to filter out the even numbers, then square the remaining numbers. Here’s how you can chain filter and map to achieve this:

let numbers = [1, 2, 3, 4, 5, 6]

let result = numbers
.filter { $0 % 2 == 0 } // Keep only even numbers
.map { $0 * $0 } // Square each remaining number

print(result) // [4, 16, 36]

In this example, we first use filter to keep only even numbers, and then we use map to square each of those even numbers. The result is a new array containing the squared values of the even numbers.

Chaining higher-order functions in Swift allows you to express complex data transformations in a clear and elegant manner. By carefully arranging these functions in a sequence, you can perform intricate operations while keeping your code concise and highly readable. This technique is a valuable tool for any Swift developer, making functional programming a powerful and expressive approach to working with data.

Custom Higher-Order Functions

You can also create custom higher-order functions to encapsulate specific behavior and enhance code reusability. Here’s an example of a custom filter function:

extension Array {
func customFilter(_ isIncluded: (Element) -> Bool) -> [Element] {
var result = [Element]()
for element in self {
if isIncluded(element) {
result.append(element)
}
}
return result
}
}

let numbers = [1, 2, 3, 4, 5]
let evenNumbers = numbers.customFilter { $0 % 2 == 0 }

print(evenNumbers) // [2, 4]

In this example, an extension for arrays has been defined to create a custom higher-order function called customFilter. This function allows you to filter elements in an array based on a custom condition defined by a closure. The closure, named isIncluded, is applied to each element in the array, and if the condition is met (returns true), the element is included in the result. The function returns an array containing the filtered elements.

Conclusion

Higher-order functions in Swift are powerful tools for working with collections, allowing you to write more expressive and concise code. By leveraging functions like map, filter, reduce, and others, you can manipulate and transform data more efficiently. Additionally, chaining these functions together can lead to code that is not only more readable but also more maintainable. Understanding and mastering these functions will significantly improve your Swift programming skills.

Happy Coding!

--

--

Vikram Kumar
Vikram Kumar

Written by Vikram Kumar

I am Vikram, a Senior iOS Developer at Matellio Inc. focused on writing clean and efficient code. Complex problem-solver with an analytical and driven mindset.

No responses yet