Swift Learning (9) - Closures (Improved Code Version)

Swift Learning (9) - Closures (Improved Code Version)

August 23, 2021·Jingyao Zhang
Jingyao Zhang

image

A closure is a self-contained block of function code that can be passed around and used in your code. Closures in Swift are similar to code blocks in C and Objective-C (blocks), as well as anonymous functions (lambdas) in Python.

Closures can capture and store references to any constants and variables from the context in which they are defined. This is known as capturing constants and variables. Swift manages all memory operations involved in the capturing process.

Note

If you are not familiar with the concept of capturing, don’t worry. There is a more detailed introduction in the value capturing section.

The global and nested functions introduced in the functions chapter are actually special kinds of closures. Closures in Swift come in one of three forms:

  • A global function is a closure with a name that does not capture any values.
  • A nested function is a closure with a name that can capture values from its enclosing function.
  • A closure expression is an anonymous closure written using lightweight syntax that can capture values from its surrounding context.

Closure expressions in Swift have a concise style and encourage syntax optimizations in common scenarios. The main optimizations are:

  • Inferring parameter and return value types from context
  • Implicitly returning single-expression closures, i.e., omitting the return keyword for single-expression closures
  • Shorthand argument names
  • Trailing closure syntax

Closure Expressions

When nested functions are part of a complex function, their self-contained code block definition and naming make them convenient to use. Of course, it is useful to write class function-like code structures without a complete declaration and function name, especially when coding methods that take functions as parameters.

Closure expressions are a way to construct inline closures. Their syntax is concise, and while maintaining clarity, closure expressions provide several optimized shorthand forms. The following example demonstrates this process through multiple iterations of improving the sorted(by:) example, each time using a more concise way to describe the same functionality.

Sorting Method

The Swift standard library provides a method called sorted(by:), which sorts the values in an array (of a specific type) based on the result of a closure expression provided by the developer. Once the sorting is complete, the sorted(by:) method returns a new array of the same type and size as the original, with elements in the correct order. The original array is not modified by the sorted(by:) method.

The following closure expression example uses the sorted(by:) method to sort an array of String values in reverse alphabetical order. Here is the initial array:

let names = ["Chris", "Alex", "Ewa", "Barry", "Jensen"]

The sorted(by:) method takes a closure that must accept two values of the same type as the array elements and return a Boolean value indicating whether the first parameter should come before the second after sorting. If the first parameter should come before the second, the closure should return true; otherwise, it should return false.

In this example, the array is of type String, so the sorting closure must have the type (String, String) -> Bool.

One way to provide the sorting closure is to write a regular function that matches the requirements and pass it as the parameter to sorted(by:):

func backward(_ s1: String, _ s2: String) -> Bool {
        return s1 > s2
}
var reversedNames = names.sorted(by: backward)  // reversedNames is ["Jensen", "Ewa", "Chris", "Barry", "Alex"]
print("Complex closure reverse order: \(reversedNames).")
---
output: Complex closure reverse order: ["Jensen", "Ewa", "Chris", "Barry", "Alex"].

If the first string (s1) is greater than the second string (s2), the backward(_:_:) function returns true, indicating that s1 should appear before s2 in the new array. For characters in strings, “greater than” means “appears later in alphabetical order.” This means that the letter “B” is greater than “A”, and the string “Tom” is greater than “Tim”. This closure will sort the strings in reverse alphabetical order, so “Jensen” will come before “Eva”.

However, writing such a simple expression (a > b) in this way is unnecessarily verbose. For this example, using closure expression syntax is a better way to construct an inline sorting closure.

Closure Expression Syntax

The general form of closure expression syntax is as follows:

/*
 { (parameters) -> return type in
         statements
 }
 */

Closure expression parameters can be in-out parameters, but cannot have default values. If you name a variadic parameter, you can use it, and tuples can be used as parameters and return values.

The following example shows the closure expression version of the previous backward(_:_:) function:

reversedNames = names.sorted(by: { (s1: String, s2: String) -> Bool in
        return s1 > s2
})
print("Inline closure expression reverse order: \(reversedNames).")
---
output: Inline closure expression reverse order: ["Jensen", "Ewa", "Chris", "Barry", "Alex"].

Note that the inline closure’s parameter and return value type declarations are the same as those of the backward(_:_:) function. In both cases, they are written as (s1: String, s2: String) -> Bool. However, in the inline closure expression, the function and return value types are written inside the braces, not outside.

The body of the closure is introduced by the keyword in. This keyword indicates that the closure’s parameter and return type definitions are complete, and the closure body is about to begin.

Because the body of this closure is so short, it can be rewritten as a single line:

reversedNames = names.sorted(by: {(s1: String, s2: String) -> Bool in return s1 > s2})
print("Concise inline closure expression reverse order: \(reversedNames).")
---
output: Concise inline closure expression reverse order: ["Jensen", "Ewa", "Chris", "Barry", "Alex"].

In this example, the overall call to the sorted(by:) method remains unchanged, with a pair of parentheses still enclosing the entire parameter. However, the parameter is now an inline closure.

Inferring Types from Context

Because the sorting closure is passed as a parameter to the sorted(by:) method, Swift can infer its parameter and return value types. The sorted(by:) method is called on a string array, so its parameter must be a function of type (String, String) -> Bool. This means that (String, String) and Bool do not need to be part of the closure expression definition. Since all types can be correctly inferred, the return arrow -> and the parentheses around the parameters can also be omitted:

reversedNames = names.sorted(by: {s1, s2 in return s1 > s2})
print("Implicit type inference closure expression reverse order: \(reversedNames).")
---
output: Implicit type inference closure expression reverse order: ["Jensen", "Ewa", "Chris", "Barry", "Alex"].

In fact, when a closure constructed by an inline closure expression is passed as a parameter to a function or method, the closure’s parameter and return value types can always be inferred. This means that when closures are passed as parameters to functions or methods, you almost never need to use the full format to construct an inline closure.

Nevertheless, you can still explicitly write out the full format of the closure. If the full format improves code readability, Swift encourages its use. In the case of the sorted(by:) method, it is clear that the closure is for string processing.

Implicit Return for Single-Expression Closures

Single-expression closures can omit the return keyword to implicitly return the result of the single expression. The previous example can be rewritten as:

reversedNames = names.sorted(by: {s1, s2 in s1 > s2})
print("Ultimate concise closure reverse order: \(reversedNames).")
---
output: Ultimate concise closure reverse order: ["Jensen", "Ewa", "Chris", "Barry", "Alex"].

In this example, the parameter type of the sorted(by:) method makes it clear that the closure must return a Bool value. Since the closure body contains only a single expression (s1 > s2), which returns a Bool, there is no ambiguity, and the return keyword can be omitted.

Shorthand Argument Names

Swift automatically provides shorthand argument names for inline closures, allowing you to refer to closure parameters in order as $0, $1, $2, and so on.

If you use shorthand argument names in a closure expression, you can omit the parameter list in the closure definition, and the types of the shorthand arguments will be inferred from the function type. The number of parameters the closure accepts depends on the highest-numbered shorthand argument used. The in keyword can also be omitted, as the closure expression now consists entirely of the closure body:

reversedNames = names.sorted(by: { $0 > $1 })
print("Shorthand argument closure reverse order: \(reversedNames).")
---
output: Shorthand argument closure reverse order: ["Jensen", "Ewa", "Chris", "Barry", "Alex"].

In this example, $0 and $1 represent the first and second String parameters of the closure. Since $1 is the highest-numbered shorthand argument, it is understood that the closure takes two parameters. The sorted(by:) function expects a closure with string parameters, so the types of $0 and $1 are both String.

Operator Methods

There is an even shorter way to write the above closure expression. Swift’s String type defines an implementation of the greater-than operator (>) for strings, which is a function that takes two String parameters and returns a Bool. This matches the function type required by the sorted(by:) method. Therefore, you can simply pass the greater-than operator, and Swift will automatically find the system-provided string function implementation:

reversedNames = names.sorted(by: >)

For more about operator methods, see Operator Methods


Trailing Closures

If you need to pass a long closure expression as the last parameter to a function, it is useful to use trailing closure syntax. A trailing closure is a closure expression written after the function’s parentheses, and the function supports calling it as the last parameter. When using a trailing closure, you do not need to write its parameter label:

/*
 func someFunctionThatTakesAClosure(closure: () -> Void) {
         // function body
 }
 // Calling the function without a trailing closure
 someFunctionThatTakesAClosure(closure: {
         // closure body
 })
 // Calling the function with a trailing closure
 someFunctionThatTakesAClosure {
         // closure body
 }
 */

The string sorting closure in the Closure Expression Syntax section can be rewritten in trailing closure form, outside the parentheses of the sorted(by:) method:

reversedNames = names.sorted() { $0 > $1 }
print("Trailing closure: \(reversedNames).")
---
output: Trailing closure: ["Jensen", "Ewa", "Chris", "Barry", "Alex"].

If the closure expression is the only parameter of the function or method, you can even omit the () when using a trailing closure:

reversedNames = names.sorted { $0 > $1 }
print("Trailing closure omitting (): \(reversedNames).")
---
output: Trailing closure omitting (): ["Jensen", "Ewa", "Chris", "Barry", "Alex"].

Trailing closures become especially useful when the closure is so long that it cannot be written on a single line. For example, Swift’s Array type has a map(_:) method, which takes a closure expression as its only parameter. The closure is called once for each element in the array and returns the mapped value for that element. The specific mapping and return type are specified by the closure.

After the closure is applied to each element of the array, the map(_:) method returns a new array containing the mapped values corresponding to each element of the original array.

The following example shows how to use a trailing closure with the map(_:) method to convert an Int array [16, 58, 510] into an array of corresponding String values ["OneSix", "FiveEight", "FiveOneZero"]:

let digitNames = [
        0: "Zero", 1: "One", 2: "Two", 3: "Three", 4: "Four",
        5: "Five", 6: "Six", 7: "Seven", 8: "Eight", 9: "Nine"
]
let numbers = [16, 58, 510]

The code above creates a dictionary mapping integer digits to their English names, and defines an integer array to be converted to a string array.

Now you can create the corresponding string array by passing a trailing closure to the map(_:) method of the numbers array:

let strings = numbers.map {
        (number) -> String in
        var number = number
        var output = ""
        repeat {
                output = digitNames[number % 10]! + output
                number /= 10
        } while number > 0
        return output
}
// The `strings` constant is inferred as an array of strings, i.e., [String]
// Its value is ["OneSix", "FiveEight", "FiveOneZero"]

map(_:) calls the closure expression once for each element in the array. There is no need to specify the type of the closure’s input parameter number, as it can be inferred from the array being mapped.

In this example, the local variable number gets its value from the closure’s number parameter, so it can be modified within the closure body (closure or function parameters are always constants). The closure expression specifies a return type of String, indicating that the new array of mapped values will be of type String.

Each time the closure is called, it creates a string called output and returns it. It uses the remainder operator (number % 10) to get the last digit and uses the digitNames dictionary to get the corresponding string. This closure can be used to create a string representation of any positive integer.

Note

The subscript on the digitNames dictionary is followed by an exclamation mark (!) because the dictionary subscript returns an optional value, indicating that the key might not exist. In this example, since number % 10 is always a valid key for the digitNames dictionary, the exclamation mark is used to force-unwrap the optional value.

The string obtained from the digitNames dictionary is added to the front of output, building a string representation of the number in reverse order. (In the expression number % 10, if number is 16, it returns 6; for 58, it returns 8; for 510, it returns 0.)

The number variable is then divided by 10. Since it is an integer, any remainder is ignored, so 16 becomes 1, 58 becomes 5, and 510 becomes 51.

The process repeats until number /= 10 is 0, at which point the closure returns the string output, and the map(_:) method adds the string to the mapped array.

In the example above, the trailing closure syntax elegantly encapsulates the closure’s functionality after the function, without needing to wrap the entire closure inside the parentheses of the map(_:) method.


Value Capturing

Closures can capture constants or variables from the context in which they are defined. Even if the original scope of those constants and variables no longer exists, the closure can still reference and modify them within its body.

In Swift, the simplest form of a closure that can capture values is a nested function, i.e., a function defined inside the body of another function. Nested functions can capture all parameters, constants, and variables defined in their enclosing function.

For example, here is a function called makeIncrementer that contains a nested function called incrementer. The nested function incrementer() captures two values from its context: runningTotal and amount. After capturing these values, makeIncrementer returns incrementer as a closure. Each time incrementer is called, it increases the value of runningTotal by amount.

func makeIncrementer(forIncrement amount: Int) -> () -> Int {
        var runningTotal = 0
        func incrementer() -> Int {
                runningTotal += amount
                return runningTotal
        }
        return incrementer
}

var testIncrementer = makeIncrementer(forIncrement: 10)
testIncrementer()  // testIncrementer() is now 10
print(testIncrementer())  // prints 20, 10 + 10
---
output: 20

makeIncrementer has a return type of () -> Int, meaning it returns a function, not a simple value. Each time the function is called, it takes no parameters and returns an Int. For more about functions returning other functions, see Function Types as Return Types.

The makeIncrementer(forIncrement:) function defines an integer variable runningTotal initialized to 0, used to store the current total. This value is returned by incrementer.

makeIncrementer(forIncrementer:) has an Int parameter, with an external name forIncrement and an internal name amount, indicating how much runningTotal will increase each time incrementer is called. The function also defines a nested function incrementer to perform the actual increment. This function simply adds amount to runningTotal and returns it.

If we look at the nested function incrementer() in isolation, we see something unusual:

/*
func incrementer() -> Int {
        runningTotal += amount
        return runningTotal
}
 */

The incrementer() function has no parameters, but it accesses the variables runningTotal and amount in its body. This is because it captures references to runningTotal and amount from the enclosing function. Capturing these references ensures that runningTotal and amount do not disappear after makeIncrementer returns, and that runningTotal still exists the next time incrementer is called.

Note

For optimization, if a value is not changed by the closure, or does not change after the closure is created, Swift may capture and save a copy of the value.

Swift also manages all memory operations for captured variables, including releasing variables that are no longer needed.

Here is an example of using makeIncrementer:

let incrementByTen = makeIncrementer(forIncrement: 10)

This example defines a constant called incrementByTen, which points to an incrementer function that increases its runningTotal variable by 10 each time it is called. Calling this function multiple times produces the following results:

incrementByTen()  // returns 10
incrementByTen()  // returns 20
var result = incrementByTen()  // returns 30
print("The result of calling IncrementByTen() three times is: \(result).")
---
output: The result of calling IncrementByTen() three times is: 30.

If you create another incrementer, it will have its own reference to a new, independent runningTotal variable:

let incrementBySeven = makeIncrementer(forIncrement: 7)
result = incrementBySeven()  // returns 7
print("Result: \(result).")
---
output: Result: 7.

Calling the original incrementByTen again will continue to increase its own runningTotal variable, which has no connection to the variable captured by incrementBySeven:

incrementByTen()  // returns 40

Note

If you assign a closure to a property of a class instance, and the closure captures the instance by accessing the instance or its members, a strong reference cycle will be created between the closure and the instance. Swift uses capture lists to break such strong reference cycles.


Closures Are Reference Types

In the example above, both incrementBySeven and incrementByTen are constants, but the closures they point to can still increment their captured variables. This is because functions and closures are reference types.

Whether you assign a function or closure to a constant or variable, you are actually setting the value of the variable or constant to a reference to the function or closure. In the example above, the reference to the closure, incrementByTen, is a constant, not the closure content itself.

This also means that if you assign a closure to two different constants or variables, both values will refer to the same closure:

let alsoIncrementByTen = incrementByTen
print("The value of alsoIncrementByTen is: \(alsoIncrementByTen()).")
---
output: The value of alsoIncrementByTen is: 50.

Escaping Closures

When a closure is passed as a parameter to a function, but the closure is executed after the function returns, the closure is said to escape from the function. When defining a function that accepts a closure as a parameter, you can mark the parameter with @escaping to indicate that the closure is allowed to “escape” from the function.

One way for a closure to escape from a function is to store it in a variable defined outside the function. For example, many functions that start asynchronous operations accept a closure parameter as a completion handler. These functions return immediately after starting the asynchronous operation, but the closure is not called until the operation finishes. In this case, the closure needs to escape from the function because it will be called after the function returns. For example:

var completionHandlers: [() -> Void] = []
func someFunctionWithEscapingClosure(completionHandler: @escaping () -> Void) {
        completionHandlers.append(completionHandler)
}

The someFunctionWithEscapingClosure(_:) function takes a closure as a parameter and adds it to an array defined outside the function. If you do not mark this parameter as @escaping, you will get a compile error (Converting non-escaping parameter ‘completionHandler’ to generic parameter ‘Element’ may allow it to escape).

Marking a closure as @escaping means you must explicitly reference self inside the closure. For example, in the code below, the closure passed to someFunctionWithEscapingClosure(_:) is an escaping closure, so it must explicitly reference self. In contrast, the closure passed to someFunctionWithNoneEscapingClosure(_:) is a non-escaping closure, so it can implicitly reference self.

func someFunctionWithNoneEscapingClosure(closure: () -> Void) {
        closure()
}
class someClass {
        var x = 10
        func doSometing() {
                someFunctionWithEscapingClosure {
                        self.x = 100
                }
                someFunctionWithNoneEscapingClosure {
                        x = 200
                }
        }
}
let instance = someClass()
instance.doSometing()
print(instance.x)  // prints "200"
completionHandlers.first?()  // the closure in someFunctionWithEscapingClosure has escaped and can be called after the function returns
print(instance.x)  // prints "100"
---
output: 200
100

Autoclosures

An autoclosure is an automatically created closure that wraps an expression passed to a function as a parameter. Such a closure takes no parameters and, when called, returns the value of the expression wrapped inside it. This convenient syntax allows you to omit the braces for the closure and use a regular expression instead of an explicit closure.

We often call functions that take autoclosures, but rarely implement such functions ourselves. For example, the assert(condition:message:file:line:) function takes autoclosures as its condition and message parameters; its condition parameter is only evaluated in debug mode, and its message parameter is only evaluated if condition is false.

Autoclosures allow developers to delay evaluation, because the code is not executed until the closure is called. Delayed evaluation is useful for code with side effects or high computational cost, as it allows developers to control when the code is executed. The following code shows how closures can delay evaluation:

var customersInLine = ["Chris", "Jensen", "Ewa", "Alex", "Barry"]
print(customersInLine.count)  // prints "5"
let customerProvider = { customersInLine.remove(at: 0) }
print(customersInLine.count)  // prints "5"
print("NOW SERVING \(customerProvider())!")  // prints "NOW SERVING Chris!"
print(customersInLine.count)  // prints "4"
---
output: 5
5
NOW SERVING Chris!
4

Although the code in the closure removes the first element of customersInLine, the element is not removed until the closure is called. If the closure is never called, the expression inside the closure will never be executed, meaning the element will never be removed from the list. Note that the type of customerProvider is not String, but () -> String, a function that takes no parameters and returns a String.

You get the same delayed evaluation behavior when passing a closure as a parameter to a function.

// customersInLine is ["Jensen", "Ewa", "Alex", "Barry"]
func serve(customer customerProvider: () -> String) {
        print("Now serving \(customerProvider())!")
}
serve(customer: { customersInLine.remove(at: 0) })  // prints "Now serving Jensen!"
---
output: Now serving Jensen!

The serve(customer:) function above takes an explicit closure that returns a customer’s name. The following version of serve(customer:) does the same thing, but instead of taking an explicit closure, it takes an autoclosure by marking the parameter with @autoclosure. Now you can call the function as if it takes a String parameter (not a closure). The customerProvider parameter is automatically converted to a closure because it is marked with the @autoclosure attribute.

// customersInLine is ["Ewa", "Alex", "Barry"]
func serve(customer customerProvider: @autoclosure () -> String) {
        print("Now Serving \(customerProvider())!")
}
serve(customer: customersInLine.remove(at: 0))  // prints "Now Serving Ewa!"
---
output: Now Serving Ewa!

Note

Overusing autoclosures can make code harder to understand. The context and function name should make it clear that evaluation is delayed.

If you want an autoclosure to escape, you should use both @autoclosure and @escaping. For an explanation of @escaping, see the section on escaping closures above.

// customersInLine is ["Alex", "Barry"]
var customerProviders: [() -> String] = []
func collectCustomerProviders(_ customerProvider: @autoclosure @escaping () -> String) {
        customerProviders.append(customerProvider)
}
collectCustomerProviders(customersInLine.remove(at: 0))
collectCustomerProviders(customersInLine.remove(at: 0))
print("Collected \(customerProviders.count) closures.")  // prints "Collected 2 closures." Now two closures have been added to the external customerProviders array
print(customersInLine)  // customersInLine is still ["Alex", "Barry"]
for customerProvider in customerProviders {
        print("Now serving \(customerProvider())!")
}  // prints "Now serving Alex!"  // prints "Now serving Barry!"
//print(customersInLine)  // customersInLine is now empty
---
output: Collected 2 closures.
["Alex", "Barry"]
Now serving Alex!
Now serving Barry!

In the code above, the collectCustomerProviders(_:) function does not call the passed-in customerProvider closure, but instead appends it to the customerProviders array, which is defined outside the function’s scope. This means the closures in the array can be called after the function returns. Therefore, the customerProvider parameter must be allowed to escape the function scope.

Last updated on