What are Higher Order Functions in Swift?
Higher order functions are functions that take one or more functions as arguments or return a function as its result. They are powerful tools that can help us write concise and expressive code. Swift provides some built-in higher order functions for collections, such as forEach
, map
, compactMap
, flatMap
, filter
, reduce
, sorted
and more.
Higher order functions enable developers to abstract common patterns of function applications and make code more concise and reusable. In Swift, higher order functions are a powerful tool for manipulating and transforming collections of data.
In this post, we will learn how to create our own higher order functions for custom purposes by implementing some examples.
Example 1: myMap
Let’s say we want to create a higher order function that applies a given function to every element of an array and returns a new array with the results. This is similar to the map
function, but we will call it myMap
for distinction. Here is how we can implement it:
// Define a generic function that takes an array and a function as arguments
// and returns a new array with the results of applying the function to each element
func myMap<T, U>(_ array: [T], _ function: (T) -> U) -> [U] {
// Create an empty array to store the results
var result = [U]()
// Loop through the array
for element in array {
// Apply the function to the element and append the result to the result array
result.append(function(element))
}
// Return the result array
return result
}
// Test the myMap function with some examples
let numbers = [1, 2, 3, 4, 5]
let strings = ["apple", "banana", "carrot", "date", "elderberry"]
// Apply a function that doubles each number
let doubled = myMap(numbers) { $0 * 2 }
print(doubled) // [2, 4, 6, 8, 10]
// Apply a function that reverses each string
let reversed = myMap(strings) {String($0.reversed()) }
print(reversed) // ["elppa", "ananab", "torrac", "etad", "yrrebredle"]
As you can see, the myMap
function is generic and can work with any types of elements and functions. We can also use shorthand arguments like $0
to make the code more concise.
We can also add an extension on Array to make it easier to use the myMap
function:
// Define an extension on Array that adds a myMap method
extension Array {
// Define a generic method that takes a function as an argument
// and returns a new array with the results of applying the function to each element
func myMap<U>(_ function: (Element) -> U) -> [U] {
// Create an empty array to store the results
var result = [U]()
// Loop through the array
for element in self {
// Apply the function to the element and append the result to the result array
result.append(function(element))
}
// Return the result array
return result
}
}
// Test the myMap method with some examples
let numbers = [1, 2, 3, 4, 5]
let strings = ["apple", "banana", "carrot", "date", "elderberry"]
// Apply a function that doubles each number using dot syntax
let doubled = numbers.myMap { $0 * 2 }
print(doubled) // [2, 4, 6, 8, 10]
// Apply a function that reverses each string using dot syntax
let reversed = strings.myMap {String($0.reversed()) }
print(reversed) // ["elppa", "ananab", "torrac", "etad", "yrrebredle"]
Example 2: myCompactMap
Another common higher order function is compactMap
, which takes an array and a transform function as arguments and returns a new array with only the non-nil results of applying the transform function to each element. This is useful for filtering out nil values from an array. Swift already provides this function for collections, but let’s see how we can implement it ourselves:
// Define a generic function that takes an array and a transform function as arguments
// and returns a new array with only the non-nil results of applying the transform function to each element
func myCompactMap<T, U>(_ array: [T], _ transform: (T) -> U?) -> [U] {
// Create an empty array to store the results
var result = [U]()
// Loop through the array
for element in array {
// Apply the transform function to the element and check if it returns a non-nil value
if let value = transform(element) {
// If yes, append it to the result array
result.append(value)
}
}
// Return the result array
return result
}
// Test the myCompactMap function with some examples
let numbers = [1, 2, nil, 4, nil]
let strings = ["apple", "", "carrot", nil, "elderberry"]
// Apply a transform function that squares each number if it is not nil
let squared = myCompactMap(numbers) { $0.map { $0 * $0 } }
print(squared) // [1, 4, 16]
// Apply a transform function that returns each string if it is not empty or nil
let nonEmpty = myCompactMap(strings) { $0?.isEmpty == false ? $0 : nil }
print(nonEmpty) // ["apple", "carrot", "elderberry"]
The myCompactMap
function is also generic and can work with any types of elements and transform functions. We can also use shorthand arguments like $0
and optional chaining like $0?
to make the code more concise.
We can also add an extension on Array to make it easier to use the myCompactMap
function:
// Define an extension on Array that adds a myCompactMap method
extension Array {
// Define a generic function that takes an array and a transform function as arguments
// and returns a new array with only the non-nil results of applying the transform function to each element
func myCompactMap<U>(_ transform: (Element) -> U?) -> [U] {
// Create an empty array to store the results
var result = [U]()
// Loop through the array
for element in self {
// Apply the transform function to the element and check if it returns a non-nil value
if let value = transform(element) {
// If yes, append it to the result array
result.append(value)
}
}
// Return the result array
return result
}
}
// Test the myCompactMap method with some examples
let numbers = [1, 2, nil, 4, nil]
let strings = ["apple", "", "carrot", nil, "elderberry"]
// Apply a transform function that squares each number if it is not nil using dot syntax
let squared = numbers.myCompactMap { $0.map { $0 * $0 } }
print(squared) // [1, 4, 16]
// Apply a transform function that returns each string if it is not empty or nil using dot syntax
let nonEmpty = strings.myCompactMap { $0?.isEmpty == false ? $0 : nil }
print(nonEmpty) // ["apple", "carrot", "elderberry"]
Example 3: myFilter
Another useful higher order function is filter
, which takes an array and a predicate function as arguments and returns a new array with only the elements that satisfy the predicate. Swift also provides this function for collections, but let’s see how we can implement it ourselves:
// Define a generic function that takes an array and a predicate function as arguments
// and returns a new array with only the elements that satisfy the predicate
func myFilter<T>(_ array: [T], _ predicate: (T) -> Bool) -> [T] {
// Create an empty array to store the results
var result = [T]()
// Loop through the array
for element in array {
// Check if the element satisfies the predicate
if predicate(element) {
// If yes, append it to the result array
result.append(element)
}
}
// Return the result array
return result
}
// Test the filter function with some examples
let numbers = [1, 2, 3, 4, 5]
let strings = ["apple", "banana", "carrot", "date", "elderberry"]
// Filter out the odd numbers using dot syntax
let even = myFilter(numbers) { $0 % 2 == 0 }
print(even) // [2,4]
// Apply a function that checks if a string has more than 5 letters
let long = myFilter(strings) { $0.count > 5 }
print(long) // ["banana", "carrot", "elderberry"]
We can also add an extension on Array to make it easier to use the myFilter
function:
// Define an extension on Array that adds a myFilter method
extension Array {
// Define a generic function that takes an array and a predicate function as arguments
// and returns a new array with only the elements that satisfy the predicate
func myFilter(_ predicate: (Element) -> Bool) -> [Element] {
// Create an empty array to store the results
var result = [Element]()
// Loop through the array
for element in self {
// Check if the element satisfies the predicate
if predicate(element) {
// If yes, append it to the result array
result.append(element)
}
}
// Return the result array
return result
}
}
// Test the myFilter method with some examples
let numbers = [1, 2, 3, 4, 5]
let strings = ["apple", "banana", "carrot", "date", "elderberry"]
// Apply a function that checks if a number is even using dot syntax
let even = numbers.myFilter { $0 % 2 == 0 }
print(even) // [2, 4]
// Apply a function that checks if a string has more than 5 letters using dot syntax
let long = strings.myFilter { $0.count > 5 }
print(long) // ["banana", "carrot", "elderberry"]
Example 4: myReduce
Another useful higher order function is reduce
, which takes an array, an initial value and a combining function as arguments and returns a single value by applying the function to each element and accumulating the result. Here is my attempt:
// Define a generic function that takes an array, an initial value and a combining function as arguments
// and returns a single value by applying the function to each element and accumulating the result
func myReduce<T, U>(_ array: [T], _ initial: U, _ combine: (U, T) -> U) -> U {
// Create a variable to store the result
var result = initial
// Loop through the array
for element in array {
// Update the result by applying the combining function to the current result and element
result = combine(result, element)
}
// Return the result
return result
}
// Test the reduce function with some examples
let numbers = [1, 2, 3, 4, 5]
let strings = ["apple", "banana", "carrot", "date", "elderberry"]
// Sum up the numbers using dot syntax
let sum = myReduce(numbers, 0, { $0 + $1 })
print(sum) // 15
// Concatenate the strings using dot syntax
let concat = myReduce(strings, "", { $0 + $1 })
print(concat) // applebananacarrotdateelderberry
We can also add an extension on Array to make it easier to use the myReduce
function. Here is how we can implement it:
// Define an extension on Array that adds a myReduce method
extension Array {
// Define a generic method that takes an initial value and a combining function as arguments
// and returns a single value by applying the function to each element and accumulating the result
func myReduceExtension<U>(_ initial: U, _ combine: (U, Element) -> U) -> U {
// Use the previously defined myReduce function and pass self as the array argument
return myReduce(self, initial, combine)
}
}
// Test the reduce method with some examples
let numbers = [1, 2, 3, 4, 5]
let strings = ["apple", "banana", "carrot", "date", "elderberry"]
// Sum up the numbers using dot syntax
let sum = numbers.myReduceExtension(0) { $0 + $1 }
print(sum) // 15
// Concatenate the strings using dot syntax
let concat = strings.myReduceExtension("") { $0 + $1 }
print(concat) // applebananacarrotdateelderberry
Example 5: mySorted
Another useful higher order function is sorted
, which takes an array and a sorting closure as arguments and returns a new array with the elements sorted according to the closure. Here is my attempt:
// Define a generic function that takes an array and a sorting closure as arguments
// and returns a new array with the elements sorted according to the closure
func mySorted<T>(_ array: [T], _ sort: (T, T) -> Bool) -> [T] {
// Create a variable to store the sorted array
var sorted = array
// Loop through the array from the first element to the second last element
for i in 0..<sorted.count - 1 {
// Loop through the array from the next element to the last element
for j in i + 1..<sorted.count {
// Compare the current pair of elements using the sorting closure
if sort(sorted[j], sorted[i]) {
// Swap the elements if they are out of order
let temp = sorted[i]
sorted[i] = sorted[j]
sorted[j] = temp
}
}
}
// Return the sorted array
return sorted
}
// Test the mySorted function with some examples
let numbers = [4, 3, 1, 5, 2]
let strings = ["apple", "banana", "carrot", "date", "elderberry"]
// Sort the numbers in ascending order using dot syntax
let ascending = mySorted(numbers, { $0 < $1 })
print(ascending) // [1, 2, 3, 4, 5]
// Sort the strings in descending order using dot syntax
let descending = mySorted(strings, { $0 > $1 })
print(descending) // ["elderberry", "date", "carrot", "banana", "apple"]
We can also add an extension on Array to make it easier to use the mySorted function. Here is how we can implement it:
// Define an extension on Array that adds a mySorted method
extension Array {
// Define a generic method that takes a sorting closure as an argument
// and returns a new array with the elements sorted according to the closure
func mySortedExtension(_ sort: (Element, Element) -> Bool) -> [Element] {
// Use the previously defined mySorted function and pass self as the array argument
return mySorted(self, sort)
}
}
// Test the mySorted method with some examples
let numbers = [4, 3, 1, 5, 2]
let strings = ["apple", "banana", "carrot", "date", "elderberry"]
// Sort the numbers in ascending order using dot syntax
let ascending = numbers.mySortedExtension { $0 < $1 }
print(ascending) // [1, 2, 3, 4, 5]
// Sort the strings in descending order using dot syntax
let descending = strings.mySortedExtension { $0 > $1 }
print(descending) // ["elderberry", "date", "carrot", "banana", "apple"]
Thanks for reading!