Chapters

Hide chapters

Expert Swift

First Edition · iOS 14 · Swift 5.4 · Xcode 12.5

10. Higher-Order Functions
Written by Ehab Amer

Heads up... You're reading this book for free, with parts of this chapter shown beyond this point as scrambled text.

A higher-order function takes one or more functions as a parameter. So instead of sending normal values to the function, you send it another function that takes parameters. The normal function is called a first-order function. Too many “functions” there.

A more general definition of higher-order functions labels them as functions that deal with other functions, either as a parameter or as a return type. In this chapter, you’ll start with sending a function as a parameter. Then you’ll move to having a function return another function.

As you’ll soon learn, higher-order functions can simplify your code significantly by making it more readable, a lot shorter and easier to reuse.

A simple text printer

Before going deeply into what higher-order functions are or how Swift makes them fun, consider this example for a text printer.

Create a new playground and add the following:

class TextPrinter {
  var formatter: ParagraphFormatterProtocol
  init(formatter: ParagraphFormatterProtocol) {
    self.formatter = formatter
  }

  func printText(_ paragraphs: [String]) {
    for text in paragraphs {
      let formattedText = formatter.formatParagraph(text)
      print(formattedText)
    }
  }
}

protocol ParagraphFormatterProtocol {
  func formatParagraph(_ text: String) -> String
}

The code above is for a text printer. All it does is receive an array of strings, run each through a formatter and then print the formatted value. TextPrinter doesn’t specify how the formatting operation is done. But it does specify that it needs an object that conforms to ParagraphFormatterProtocol, which will implement this functionality.

Add this new class to your playground:

class SimpleFormatter: ParagraphFormatterProtocol {
  func formatParagraph(_ text: String) -> String {
    guard !text.isEmpty else { return text } // 1
    var formattedText =
      text.prefix(1).uppercased() + text.dropFirst() // 2
    if let lastCharacter = formattedText.last,
       !lastCharacter.isPunctuation {
      formattedText += "." // 3
    }
    return formattedText
  }
}

This simple formatter conforms to the required protocol, and its format function does the following:

  1. Makes sure the provided string is not empty. If it is, then there’s nothing to format and the formatter returns the string as-is.
  2. Capitalizes the first character
  3. Checks whether the last character is punctuation. If it isn’t, then it adds a full-stop “.”.

Now, use this new class in your playground:

let simpleFormatter = SimpleFormatter()
let textPrinter = TextPrinter(formatter: simpleFormatter)

let exampleParagraphs = [
  "basic text example",
  "Another text example!!",
  "one more text example"
]

textPrinter.printText(exampleParagraphs)

You created an instance of the simple formatter you just defined and used it to create an object of the printer. The output in the console from the code above should be:

Basic text example.
Another text example!!
One more text example.

First-order functions

The two main methods here are first-order functions. Both ParagraphFormatterProtocol.formatParagraph(_:) and TextPrinter.printText(_:) take normal values. You didn’t go into the higher-order ones yet.

extension Array where Element == String {
  func printFormatted(formatter: ParagraphFormatterProtocol) {
    let textPrinter = TextPrinter(formatter: formatter)
    textPrinter.printText(self)
  }
}
exampleParagraphs.printFormatted(formatter: simpleFormatter)

Your first higher-order function

Create a new playground and try this in a new way. Extract the formatting function you created in SimpleFormatter from the previous example and add it as a function:

func formatParagraph(_ text: String) -> String {
  guard !text.isEmpty else { return text }
  var formattedText = 
    text.prefix(1).uppercased() + text.dropFirst()
  if let lastCharacter = formattedText.last,
     !lastCharacter.isPunctuation {
    formattedText += "."
  }
  return formattedText
}
extension Array where Element == String {
  func printFormatted(formatter: ((String) -> String)) {
    for string in self {
      let formattedString = formatter(string)
      print(formattedString)
    }
  }
}
let exampleParagraphs = [
  "basic text example",
  "Another text example!!",
  "one more text example"
]

exampleParagraphs.printFormatted(formatter: formatParagraph(_:))
let theFunction = formatParagraph
exampleParagraphs.printFormatted(formatter: theFunction)

Closures

There’s also another form of passing over the function as a parameter that you’re familiar with:

exampleParagraphs.printFormatted { text in
  guard !text.isEmpty else { return text }
  var formattedText = text.prefix(1).uppercased() + text.dropFirst()
  if let lastCharacter = formattedText.last,
     !lastCharacter.isPunctuation {
    formattedText += "."
  }
  return formattedText
}
exampleParagraphs.printFormatted { formatParagraph($0) }
aHigherOrderFunction { someOperation($0, "a constant") }

Higher-order functions in the standard library

Swift brought you a few higher-order functions, which deliver several common operations and have a neat way to call them in your code. Some of those functions are:

map

Array.map(_:) applies an operation on all the elements of the array, and the result is a new array of the same size. This is a much shorter version of iterating over the elements and adding the new items of an operation to a new array. Try this example in a playground:

var numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
var newNumbers: [Int] = []

for number in numbers {
  newNumbers.append(number * number)
}

print(newNumbers)
let newNumbers2 = numbers.map { $0 * $0 }
print(newNumbers2)
func squareOperation(value: Int) -> Int {
  print("Original Value is: \(value)")
  let newValue = value * value
  print("New Value is: \(newValue)")
  return newValue
}
let newNumbers3 = numbers.map(squareOperation(value:))

compactMap

Array.compactMap(_:) is like the previous one, but the result array doesn’t need to be the same size as the original array. This one filters out nil values from the resulting operation. The operation in the parameter has an optional result, but map(_:) didn’t allow for that.

func wordsToInt(_ str: String) -> Int? {
  let formatter = NumberFormatter()
  formatter.numberStyle = .spellOut
  return formatter.number(from: str.lowercased()) as? Int
}
wordsToInt("Three") // 3
wordsToInt("Four") // 4
wordsToInt("Five") // 5
wordsToInt("Hello") // nil
func convertToInt(_ value: Any) -> Int? {
  if let value = value as? String {
    return wordsToInt(value)
  } else {
    return value as? Int
  }
}
convertToInt("one") // 1
convertToInt(1.1) // nil
convertToInt(1) // 1
let sampleArray: [Any] = [1, 2, 3.0, "Four", "Five", "sixx", 7.1, "Hello", "World", "!"]

let newArray = sampleArray.compactMap(convertToInt(_:)) // [1, 2, 4, 5]
func convertToInt(_ value: Any) -> Int? {
  if let value = value as? String {
    return wordsToInt(value)
  } else if let value = value as? Double {
    return Int(value)
  } else {
    return value as? Int
  }
}

flatMap

As you saw in compactMap(_:), you can have fewer results than in your original array. But what if the operation you want will provide an array for each item? Consider that you want to calculate the square, the cube and the fourth power of each number in an array. Consider the following function:

func calculatePowers(_ number: Int) -> [Int] {
  var results: [Int] = []
  var value = number
  for _ in 0...2 {
    value *= number
    results.append(value)
  }
  return results
}
calculatePowers(3) // [9, 27, 81]
let exampleList = [1, 2, 3, 4, 5]
let result = exampleList.map(calculatePowers(_:))
// [[1, 1, 1], [4, 8, 16], [9, 27, 81], [16, 64, 256], [25, 125, 625]]
result.count // 5
let joinedResult = Array(result.joined())
// [1, 1, 1, 4, 8, 16, 9, 27, 81, 16, 64, 256, 25, 125, 625]
let flatResult = exampleList.flatMap(calculatePowers(_:))
// [1, 1, 1, 4, 8, 16, 9, 27, 81, 16, 64, 256, 25, 125, 625]

filter

filter(_:) is one of the simplest higher-order functions. As its name suggests, you want to filter a collection of many items based on criteria. If the element meets the criteria, keep it. But if it doesn’t, remove it from the list.

func intToWord(_ number: Int) -> String? {
  let formatter = NumberFormatter()
  formatter.numberStyle = .spellOut
  return formatter.string(from: number as NSNumber)
}

let numbers: [Int] = Array(0...100)
let words = numbers.compactMap(intToWord(_:))
// ["zero", "one", "two", ....., "ninety-nine", "one hundred"]
func shouldKeep(word: String) -> Bool {
  return word.count == 4
}
let filteredWords = words.filter(shouldKeep(word:))
// ["zero", "four", "five", "nine"]
let filteredWords = words.filter { $0.count == 4 }

reduce

reduce(_:_:) is a handy method when you want to merge a group of elements. Common examples include adding or multiplying a group of numbers. But this doesn’t mean it works only on numbers. You also can use it on your own custom types. Say you have scores for a soccer team and you want to merge them all to see the team’s totals. Consider this Score type:

struct Score {
  var wins = 0, draws = 0, losses = 0
  var goalsScored = 0, goalsReceived = 0

  init() {}

  init(goalsScored: Int, goalsReceived: Int) {
    self.goalsScored = goalsScored
    self.goalsReceived = goalsReceived

    if goalsScored == goalsReceived {
      draws = 1
    } else if goalsScored > goalsReceived {
      wins = 1
    } else {
      losses = 1
    }
  }
}
var teamScores = [
  Score(goalsScored: 1, goalsReceived: 0),
  Score(goalsScored: 2, goalsReceived: 1),
  Score(goalsScored: 0, goalsReceived: 0),
  Score(goalsScored: 1, goalsReceived: 3),
  Score(goalsScored: 2, goalsReceived: 2),
  Score(goalsScored: 3, goalsReceived: 0),
  Score(goalsScored: 4, goalsReceived: 3)
]
extension Score {
  static func +(left: Score, right: Score) -> Score {
    var newScore = Score()

    newScore.wins = left.wins + right.wins
    newScore.losses = left.losses + right.losses
    newScore.draws = left.draws + right.draws
    newScore.goalsScored = 
      left.goalsScored + right.goalsScored
    newScore.goalsReceived = 
      left.goalsReceived + right.goalsReceived

    return newScore
  }
}
let firstSeasonScores = teamScores.reduce(Score(), +)
// Score(wins: 4, draws: 2, losses: 1, goalsScored: 13, goalsReceived: 9)
var secondSeasonMatches = [
  Score(goalsScored: 5, goalsReceived: 3),
  Score(goalsScored: 1, goalsReceived: 1),
  Score(goalsScored: 0, goalsReceived: 2),
  Score(goalsScored: 2, goalsReceived: 0),
  Score(goalsScored: 2, goalsReceived: 2),
  Score(goalsScored: 3, goalsReceived: 2),
  Score(goalsScored: 2, goalsReceived: 3)
]

let totalScores = secondSeasonMatches.reduce(firstSeasonScores, +)
// Score(wins: 7, draws: 4, losses: 3, goalsScored: 28, goalsReceived: 22)

sorted

This might be one of the most frequently used functions in day-to-day work. It sorts an array of elements for you.

let words = ["zero", "one", "two", "three", "four", "five", "six", "seven", "eight", "nine", "ten"]

let stringOrderedWords = words.sorted()
// ["eight", "five", "four", "nine", "one", "seven", "six", "ten", "three", "two", "zero"]
func areMatchesSorted(first: Score, second: Score) -> Bool {
  if first.wins != second.wins { // 1
    return first.wins > second.wins
  } else if first.draws != second.draws { // 2
    return first.draws > second.draws
  } else { // 3
    let firstDifference = first.goalsScored - first.goalsReceived
    let secondDifference = second.goalsScored - second.goalsReceived

    if firstDifference == secondDifference {
      return first.goalsScored > second.goalsScored
    } else {
      return firstDifference > secondDifference
    }
  }
}
var teamScores = [
  Score(goalsScored: 1, goalsReceived: 0),
  Score(goalsScored: 2, goalsReceived: 1),
  Score(goalsScored: 0, goalsReceived: 0),
  Score(goalsScored: 1, goalsReceived: 3),
  Score(goalsScored: 2, goalsReceived: 2),
  Score(goalsScored: 3, goalsReceived: 0),
  Score(goalsScored: 4, goalsReceived: 3)
]

let sortedMatches = teamScores.sorted(by: areMatchesSorted(first:second:))
//[Score(wins: 1, draws: 0, losses: 0, goalsScored: 3, goalsReceived: 0),
// Score(wins: 1, draws: 0, losses: 0, goalsScored: 4, goalsReceived: 3),
// Score(wins: 1, draws: 0, losses: 0, goalsScored: 2, goalsReceived: 1),
// Score(wins: 1, draws: 0, losses: 0, goalsScored: 1, goalsReceived: 0),
// Score(wins: 0, draws: 1, losses: 0, goalsScored: 2, goalsReceived: 2),
// Score(wins: 0, draws: 1, losses: 0, goalsScored: 0, goalsReceived: 0),
// Score(wins: 0, draws: 0, losses: 1, goalsScored: 1, goalsReceived: 3)]

Function as a return type

Functions that return other functions are also higher-order functions. This might not be something you’re accustomed to. But it can heavily empower your code and make it simple, despite using an indirect approach and looking complicated at first. Make sure you have cleared your mind so it doesn’t feel too complicated.

Currying

Earlier, you learned that sending a function directly to a higher-order function requires the signatures to match. Thus, the example below is best done through a closure because the signatures don’t match.

aHigherOrderFunction { someOperation($0, "a constant") }
aHigherOrderFunction { curried_SomeOperation("a constant")($0) }
aHigherOrderFunction(curried_SomeOperation("a constant"))
func aHigherOrderFunction(_ operation: (Int) -> ()) {
  let numbers = 1...10
  numbers.forEach(operation)
}

func someOperation(_ p1: Int, _ p2: String) {
  print("number is: \(p1), and String is: \(p2)")
}

aHigherOrderFunction { someOperation($0, "a constant") }
func curried_SomeOperation(_ p1: Int) -> ((String) -> ())
func curried_SomeOperation(_ p1: Int) -> (String) -> () {
  return { str in
    print("number is: \(p1), and String is: \(str)")
  }
}
aHigherOrderFunction { curried_SomeOperation($0)("a constant") }
number is: 1, and String is: a constant
.
.
.
number is: 10, and String is: a constant
func curried_SomeOperation(_ str: String) -> (Int) -> () {
  return { p1 in
    print("number is: \(p1), and String is: \(str)")
  }
}
aHigherOrderFunction { curried_SomeOperation("a constant")($0) }
aHigherOrderFunction(curried_SomeOperation("a constant"))

A generic currying function

Using an original function’s signature originalMethod(A, B) -> C, you want to transform it to: (A) -> (B) -> C.

func curry<A, B, C>(
  _ originalMethod: (A, B) -> C
) -> (A) -> (B) -> C
func curry<A, B, C>(
  _ originalMethod: @escaping (A, B) -> C
) -> (A) -> (B) -> C {
  return { a in
    { b in
      originalMethod(a, b)
    }
  }
}
someOperation(1, "number one")
curry(someOperation)(1)("number one")
number is: 1, and String is: number one
number is: 1, and String is: number one

Generic argument flipping

Flipping won’t be as confusing as currying because you don’t need to make any drastic changes to the signature. Add the following function:

func flip<A, B, C>(
  _ originalMethod: @escaping (A) -> (B) -> C
) -> (B) -> (A) -> C {
  return { b in { a in originalMethod(a)(b) } }
}
aHigherOrderFunction(flip(curry(someOperation))("a constant"))

Generated class methods by Swift

For each method or instance-function you create, Swift creates a class higher-order function for this method. In a new playground page, add this extension:

extension Int {
  func word() -> String? {
    let formatter = NumberFormatter()
    formatter.numberStyle = .spellOut
    return formatter.string(from: self as NSNumber)
  }
}
1.word() // one
10.word() // ten
36.word() // thirty-six
Int.word // (Int) -> () -> Optional<String>
Int.word(1)() // one
Int.word(10)() // ten
Int.word(36)() // thirty-six
func flip<A, C>(
  _ originalMethod: @escaping (A) -> () -> C
) -> () -> (A) -> C {
  return { { a in originalMethod(a)() } }
}
flip(Int.word)()(1) // one
var flippedWord = flip(Int.word)()
[1, 2, 3, 4, 5].map(flippedWord)
// ["one", "two", "three", "four", "five"]
func reduce<A, C>(
  _ originalMethod: @escaping (A) -> () -> C
) -> (A) -> C {
  return { a in originalMethod(a)() }
}

var reducedWord = reduce(Int.word)

Merging higher-order functions

An interesting trick you can do with higher-order functions is to merge them. Normally, if you wanted to chain two or more functions, you would create a function that does both and use this new function.

extension Int {
  func word() -> String? {
    let formatter = NumberFormatter()
    formatter.numberStyle = .spellOut
    return formatter.string(from: self as NSNumber)
  }

  func squared() -> Int {
    return self * self
  }
}
func squareAndWord() -> String? {
  self.squared().word()
}
func mergeFunctions<A, B, C>(
  _ f: @escaping (A) -> () -> B,
  _ g: @escaping (B) -> () -> C
) -> (A) -> C {
  return { a in
    let fValue = f(a)()
    return g(fValue)()
  }
}
var mergedFunctions = mergeFunctions(Int.squared, Int.word)
mergedFunctions(2) // four
func +<A, B, C>(
  left: @escaping (A) -> () -> B,
  right: @escaping (B) -> () -> C
) -> (A) -> C {
  return { a in
    let leftValue = left(a)()
    return right(leftValue)()
  }
}
var addedFunctions = Int.squared + Int.word
addedFunctions(2) // four
(Int.squared + Int.word)(2) // four

Key points

  • A higher-order function is a function that deals with other functions, either as a parameter or as a return type.
  • Swift allows the use of a closure or a function signature in a higher-order function, as long as the number of parameters and the return type are identical to the original higher-order function declaration.
  • Using a function signature instead of a closure can simplify your code if the operation is complex or gets repeated across your code.
  • map, compactMap, flatMap, filter, reduce, sorted and split all are examples of higher-order functions in the standard library.
  • Higher-order functions also describe functions that return functions as return types.
  • Function currying means breaking down a function that takes multiple parameters into a chain of functions that each takes one parameter.
  • Currying and argument flipping are ways to alter a function’s signature to fit a higher-order function.
  • Each instance method can be used as a higher-order function through its containing type.
  • Function composition is when you merge higher-order functions to create larger functions.
  • You can use operator overloading to create an adding function for higher-order functions, making function composition easier.

Where to go from here?

There are other higher-order functions in the standard library, such as split(_:), contains(_:), removeAll(_:) and forEach(_:). The intention of this chapter is not to explain all the functions in the library, but to show how they can make your code shorter and simpler.

Have a technical question? Want to report a bug? You can ask questions and report bugs to the book authors in our official book forum here.
© 2024 Kodeco Inc.

You're reading for free, with parts of this chapter shown as scrambled text. Unlock this book, and our entire catalogue of books and videos, with a Kodeco Personal Plan.

Unlock now