What is Higher-Order Functions in Swift ?
Higher-order functions in Swift are functions that take other functions as arguments or return functions as their results.
They are powerful tools in functional programming and can help make your code more expressive and concise.
Common Higher-Order Functions in Swift
map
In Swift, the map function transforms each element in a collection using a specified closure and returns a new collection containing the transformed elements.
How Map Works:
The map function takes a closure as its argument. This closure defines how to transform each element in the original collection. The closure takes an element of the original collection as its parameter and returns the transformed element. The map function then applies this closure to each element in the collection and returns a new collection containing the results.
Syntax
Here is the basic syntax for using the map function:
let newCollection = oldCollection.map { element in
// Transformation code
return transformedElement
}
Example 1: Simple Transformation
Let’s start with a simple example where we want to transform an array of integers by squaring each element.
let numbers = [1, 2, 3, 4, 5] let squares = numbers.map { $0 * $0 } print(squares) // Output: [1, 4, 9, 16, 25]
In this example:
numbersis the original array.- The closure
{ $0 * $0 }takes each element ($0) and returns its square. mapapplies this closure to each element innumbersand returns a new array,squares, containing the squared values.
Example 2: Transforming Strings
You can also use map to transform arrays of strings. For instance, converting an array of strings to their uppercase equivalents:
let lowercaseStrings = ["apple", "banana", "cherry"] let uppercaseStrings = lowercaseStrings.map { $0.uppercased() } print(uppercaseStrings) // Output: ["APPLE", "BANANA", "CHERRY"]
In this example:
lowercaseStringsis the original array of strings.- The closure
{ $0.uppercased() }converts each string to uppercase. mapapplies this closure to each element inlowercaseStringsand returns a new array,uppercaseStrings, containing the uppercase strings.
Example 3: Complex Transformation
You can perform more complex transformations, such as converting an array of dictionaries to an array of custom objects.
struct Person { let name: String let age: Int } let peopleDicts = [ ["name": "Alice", "age": 25], ["name": "Bob", "age": 30], ["name": "Charlie", "age": 35] ] let people = peopleDicts.map { dict in Person(name: dict["name"] as! String, age: dict["age"] as! Int) } print(people) // Output: [Person(name: "Alice", age: 25), Person(name: "Bob", age: 30), Person(name: "Charlie", age: 35)]
In this example:
peopleDictsis an array of dictionaries, where each dictionary represents a person.- The closure converts each dictionary into a
Personobject by extracting thenameandagevalues and using them to initialize aPersoninstance. mapapplies this closure to each dictionary inpeopleDictsand returns a new array,people, containing thePersonobjects.
filter
The filter function in Swift allows you to create a new collection containing only the elements of an original collection that satisfy a given condition (predicate).
How filter Works
The filter function takes a closure that returns a Boolean value. This closure is applied to each element of the collection, and only those elements for which the closure returns true are included in the new collection.
Syntax
Here is the basic syntax for using the filter function:
let newCollection = oldCollection.filter { element in // Condition that returns a Boolean return condition }
Example 1: Filtering Even Numbers
Let’s start with a simple example where we want to filter an array of integers to include only the even numbers.
let numbers = [1, 2, 3, 4, 5] let evenNumbers = numbers.filter { $0 % 2 == 0 } print(evenNumbers) // Output: [2, 4]
In this example:
numbersis the original array.- The closure
{ $0 % 2 == 0 }checks if each element ($0) is even. filterapplies this closure to each element innumbersand returns a new array,evenNumbers, containing only the even numbers.
Example 2: Filtering Strings
You can also use filter to work with arrays of strings. For instance, filtering an array of strings to include only those that contain a specific substring:
let fruits = ["apple", "banana", "cherry", "date"] let fruitsWithA = fruits.filter { $0.contains("a") } print(fruitsWithA) // Output: ["apple", "banana", "date"]
In this example:
fruitsis the original array of strings.- The closure
{ $0.contains("a") }checks if each string contains the letter “a”. filterapplies this closure to each string infruitsand returns a new array,fruitsWithA, containing only the strings that include “a”.
Example 3: Filtering Custom Objects
You can also use filter with custom objects. For example, filtering an array of custom Person objects to include only those above a certain age.
struct Person { let name: String let age: Int } let people = [ Person(name: "Alice", age: 25), Person(name: "Bob", age: 30), Person(name: "Charlie", age: 35) ] let adults = people.filter { $0.age >= 30 } print(adults) // Output: [Person(name: "Bob", age: 30), Person(name: "Charlie", age: 35)]
In this example:
peopleis the original array ofPersonobjects.- The closure
{ $0.age >= 30 }checks if each person’s age is 30 or above. filterapplies this closure to eachPersonobject inpeopleand returns a new array,adults, containing only the people who are 30 or older.
reduce
The reduce function in Swift is a powerful higher-order function that allows you to combine all elements of a collection into a single value using a given closure.
This is done by applying a closure that specifies how to combine the elements.
How reduce Works
The reduce function takes two arguments:
- An initial value.
- A closure that combines the current accumulated value with each element of the collection.
The closure is applied sequentially to each element of the collection, along with the accumulated value, to produce a single result.
Syntax
Here is the basic syntax for using the reduce function:
let result = collection.reduce(initialValue) { accumulator, element in // Combine accumulator and element return newAccumulator }
Example 1: Calculating the Product of Numbers
You can use reduce to calculate the product of all elements in an array of integers.
let numbers = [1, 2, 3, 4, 5] let product = numbers.reduce(1) { $0 * $1 } print(product) // Output: 120
In this example:
numbersis the original array.1is the initial value for the product.- The closure
{ $0 * $1 }multiplies each element ($1) by the current accumulated product ($0). reduceapplies this closure to each element innumbersand returns the total product, which is120.
Detailed Explanation
- Array Definition:
let numbers = [1, 2, 3, 4, 5]
This line defines an array of integers named numbers containing the values [1, 2, 3, 4, 5].
2. Using reduce Without Shorthand i.e ($0,$1) to understand better:
let product = numbers.reduce(1, { (accumulator: Int, element: Int) -> Int in
return accumulator * element
})
This line uses the reduce function to calculate the product of all elements in the numbers array. Let’s break it down:
- Initial Value:
1The first argument toreduceis the initial value for the accumulator. In this case, the initial value is1, which is the neutral element for multiplication (multiplying by 1 doesn’t change the product). - Closure Definition:
{ (accumulator: Int, element: Int) -> Int in
return accumulator * element
}
The second argument to reduce is a closure that defines how to combine the elements of the array. This closure has two parameters:
accumulator: The current accumulated value, which starts with the initial value1.element: The current element from the array being processed.
The closure multiplies the accumulator by the element and returns the result, which becomes the new accumulator value for the next iteration.
3. Iterative Process:
The reduce function processes each element in the array as follows:
-
- Initial State:
accumulator= 1 (initial value)
- First Iteration:
element= 1accumulator= 1 * 1 = 1
- Second Iteration:
element= 2accumulator= 1 * 2 = 2
- Third Iteration:
element= 3accumulator= 2 * 3 = 6
- Fourth Iteration:
element= 4accumulator= 6 * 4 = 24
- Fifth Iteration:
element= 5accumulator= 24 * 5 = 120
- Initial State:
4. Final Result:
After processing all elements, the final value of accumulator is 120, which is the product of all elements in the numbers array.
5. Print the Result:
print(product) // Output: 120
This line prints the result, which is 120.
Example 2: Summing Numbers
Let’s start with a simple example where we want to sum all the elements in an array of integers.
let numbers = [1, 2, 3, 4, 5] let sum = numbers.reduce(0) { $0 + $1 } print(sum) // Output: 15
In this example:
numbersis the original array.0is the initial value for the sum.- The closure
{ $0 + $1 }adds each element ($1) to the current accumulated sum ($0). reduceapplies this closure to each element innumbersand returns the total sum, which is15.
Example 3: Concatenating Strings
You can also use reduce to concatenate an array of strings.
let words = ["Hello", "world", "from", "Swift"] let sentence = words.reduce("") { $0 + " " + $1 } print(sentence) // Output: " Hello world from Swift"
In this example:
wordsis the original array of strings.""(an empty string) is the initial value for the concatenated result.- The closure
{ $0 + " " + $1 }concatenates each string ($1) to the current accumulated string ($0), with a space in between. reduceapplies this closure to each string inwordsand returns the concatenated result, which is" Hello world from Swift".
Example 4: Combining Custom Objects
You can also use reduce with custom objects. For instance, calculating the total age of a group of people.
struct Person { let name: String let age: Int } let people = [ Person(name: "Alice", age: 25), Person(name: "Bob", age: 30), Person(name: "Charlie", age: 35) ] let totalAge = people.reduce(0) { $0 + $1.age } print(totalAge) // Output: 90
In this example:
peopleis the original array ofPersonobjects.0is the initial value for the total age.- The closure
{ $0 + $1.age }adds the age of eachPerson($1.age) to the current accumulated total age ($0). reduceapplies this closure to eachPersoninpeopleand returns the total age, which is90.
compactMap
The compactMap function in Swift is a higher-order function that transforms each element of a collection using a given closure and removes any resulting nil values.
This function is particularly useful for handling collections of optional values and filtering out nil results after a transformation.
How compactMap Works
The compactMap function applies a closure to each element in the collection. This closure returns an optional value. If the closure returns nil for an element, that element is excluded from the resulting collection. Otherwise, the non-nil results are included in the new collection.
Syntax
Here is the basic syntax for using the compactMap function:
let newCollection = oldCollection.compactMap { element in // Transformation code that returns an optional value return transformedElement }
Example 1: Filtering and Unwrapping Optionals
Let’s start with a simple example where we have an array of optional integers, and we want to create a new array with only the non-nil values.
let numbers: [Int?] = [1, 2, nil, 4, nil, 6] let nonNilNumbers = numbers.compactMap { $0 } print(nonNilNumbers) // Output: [1, 2, 4, 6]
In this example:
numbersis the original array containing optional integers.- The closure
{ $0 }returns each element as it is. compactMapremoves anynilvalues and unwraps the non-nilvalues, resulting in a new array,nonNilNumbers.
Example 2: Transforming and Filtering Strings
You can also use compactMap to transform and filter strings. For instance, converting an array of strings to integers and filtering out non-numeric strings.
let strings = ["1", "2", "three", "4", "five"] let numbers = strings.compactMap { Int($0) } print(numbers) // Output: [1, 2, 4]
In this example:
stringsis the original array of strings.- The closure
Int($0)attempts to convert each string to an integer, returning an optional integer. compactMapremoves anynilvalues (strings that couldn’t be converted to integers) and returns a new array,numbers, containing only the successfully converted integers.
Example 3: Combining Custom Objects
You can use compactMap with custom objects to transform and filter based on specific conditions.
struct Person { let name: String let age: Int? } let people = [ Person(name: "Alice", age: 25), Person(name: "Bob", age: nil), Person(name: "Charlie", age: 30) ] let ages = people.compactMap { $0.age } print(ages) // Output: [25, 30]
In this example:
peopleis an array ofPersonobjects, some of which havenilfor theageproperty.- The closure
{ $0.age }returns the age of each person, which is an optional integer. compactMapremoves anynilvalues and returns a new array,ages, containing only the non-nilages.
Custom Higher-Order Functions
You can also create your own higher-order functions. Here’s an example of a function that takes another function as an argument:
func applyTwice(_ function: (Int) -> Int, to value: Int) -> Int { return function(function(value)) } let result = applyTwice({ $0 * 2 }, to: 3) print(result) // Output: 12
In this example, the applyTwice function takes a function that transforms an Int and an Int value, applies the function to the value twice, and returns the result.
Custom implementation of higher-order functions in Swift
I will demonstrate custom implementations of the map, filter, reduce, flatMap, and compactMap higher-order functions in Swift. These implementations will help you understand how these functions work under the hood.
Custom Implementation of map
The map function applies a given transformation to each element of a collection and returns a new collection containing the transformed elements.
extension Array { func customMap<T>(_ transform: (Element) -> T) -> [T] { var newCollection: [T] = [] for element in self { newCollection.append(transform(element)) } return newCollection } } // Example usage: let numbers = [1, 2, 3, 4, 5] let squares = numbers.customMap { $0 * $0 } print(squares) // Output: [1, 4, 9, 16, 25]
Custom Implementation of filter
The filter function returns a new collection containing only the elements that satisfy a given predicate.
extension Array { func customFilter(_ predicate: (Element) -> Bool) -> [Element] { var result: [Element] = [] for element in self { if predicate(element) { result.append(element) } } return result } } // Example usage: let numbers = [1, 2, 3, 4, 5] let evenNumbers = numbers.customFilter { $0 % 2 == 0 } print(evenNumbers) // Output: [2, 4]
Custom Implementation of reduce
The reduce function combines all elements of a collection into a single value by applying a closure that specifies how to combine the elements.
extension Array { func customReduce<T>(_ initialValue: T, _ combine: (T, Element) -> T) -> T { var result = initialValue for element in self { result = combine(result, element) } return result } } // Example usage: let numbers = [1, 2, 3, 4, 5] let sum = numbers.customReduce(0) { $0 + $1 } print(sum) // Output: 15
Custom Implementation of compactMap
The compactMap function transforms each element in a collection and removes any resulting nil values.
extension Array { func customCompactMap<T>(_ transform: (Element) -> T?) -> [T] { var result: [T] = [] for element in self { if let value = transform(element) { result.append(value) } } return result } } //Example usage: let numbers: [Int?] = [1, 2, nil, 4, nil, 6] let nonNilNumbers1 = numbers.customCompactMap { $0 } // Shorthand //OR let nonNilNumbers2:[Int] = numbers.customCompactMap { item in guard let unwrappedItem = item else { return nil } return unwrappedItem } print(nonNilNumbers1) // Output: [1, 2, 4, 6] print(nonNilNumbers2) // Output: [1, 2, 4, 6]
Conclusion
Higher-order functions are a powerful feature in Swift that can make your code more expressive and concise. By understanding and utilizing functions like map, filter, reduce, flatMap, and custom higher-order functions, you can write more functional and elegant Swift code.
Read More:
