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:
numbers
is the original array.- The closure
{ $0 * $0 }
takes each element ($0
) and returns its square. map
applies this closure to each element innumbers
and 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:
lowercaseStrings
is the original array of strings.- The closure
{ $0.uppercased() }
converts each string to uppercase. map
applies this closure to each element inlowercaseStrings
and 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:
peopleDicts
is an array of dictionaries, where each dictionary represents a person.- The closure converts each dictionary into a
Person
object by extracting thename
andage
values and using them to initialize aPerson
instance. map
applies this closure to each dictionary inpeopleDicts
and returns a new array,people
, containing thePerson
objects.
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:
numbers
is the original array.- The closure
{ $0 % 2 == 0 }
checks if each element ($0
) is even. filter
applies this closure to each element innumbers
and 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:
fruits
is the original array of strings.- The closure
{ $0.contains("a") }
checks if each string contains the letter “a”. filter
applies this closure to each string infruits
and 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:
people
is the original array ofPerson
objects.- The closure
{ $0.age >= 30 }
checks if each person’s age is 30 or above. filter
applies this closure to eachPerson
object inpeople
and 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:
numbers
is the original array.1
is the initial value for the product.- The closure
{ $0 * $1 }
multiplies each element ($1
) by the current accumulated product ($0
). reduce
applies this closure to each element innumbers
and 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:
1
The first argument toreduce
is 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:
numbers
is the original array.0
is the initial value for the sum.- The closure
{ $0 + $1 }
adds each element ($1
) to the current accumulated sum ($0
). reduce
applies this closure to each element innumbers
and 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:
words
is 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. reduce
applies this closure to each string inwords
and 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:
people
is the original array ofPerson
objects.0
is 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
). reduce
applies this closure to eachPerson
inpeople
and 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:
numbers
is the original array containing optional integers.- The closure
{ $0 }
returns each element as it is. compactMap
removes anynil
values and unwraps the non-nil
values, 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:
strings
is the original array of strings.- The closure
Int($0)
attempts to convert each string to an integer, returning an optional integer. compactMap
removes anynil
values (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:
people
is an array ofPerson
objects, some of which havenil
for theage
property.- The closure
{ $0.age }
returns the age of each person, which is an optional integer. compactMap
removes anynil
values and returns a new array,ages
, containing only the non-nil
ages.
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: