Skip to content

Latest commit

 

History

History
311 lines (263 loc) · 16 KB

closures.md

File metadata and controls

311 lines (263 loc) · 16 KB

Published by Arunprasadh C on 19 May 2022Last Updated on 25 May 2022

Closures in Swift

Closures are self-contained blocks of functionality that can be passed around and used in our code (Similar to Lambda Expressions of Java/C/C++/Python/Kotlin). Closures can capture and store references to any Constants and Variables from the context in which they’re defined. This is known as closing over those Constants and Variables. Swift handles all of the memory management of capturing values.

Global and nested functions, as introduced in Functions, are actually special cases of Closures. Closures take one of three forms:

  • Global functions are closures that have a name and don’t capture any values.
  • Nested functions are closures that have a name and can capture values from their enclosing function.
  • Closure expressions are unnamed closures written in a lightweight syntax that can capture values from their surrounding context.

Swift’s closure expressions have a clean and clear style, with optimizations that encourage brief, clutter-free syntax in common scenarios. These optimizations include:

  • Inferring parameter and return value types from context.
  • Implicit returns from single-expression closures.
  • Shorthand argument names.
  • Trailing closure syntax.

Closure Expressions

Closure expression syntax has the following general form:

Syntax :

{ (parameters) -> returnType in
   // Set of statements
}

The in keyword is used to separate parameter declaration from the body of the closure. In-Out Parameters are allowed in Closures but Default Parameter values are not allowed in Closures. Named Variadic Parameters and Tuples can also be used with Closures. Closures make it easier to write Higher Order Functions (Functions which take Other Functions as Parameters). For example, consider the sorted(by:) Function from the Swift Standard Library for Arrays, which sorts the Array according to the comparison function given as parameter and returns the sorted array. If we are to give this comparison function as a Function Object, it would look like:

Example 1:

func decreasingOrder(lhs: Int, rhs: Int) -> Bool
{
    lhs > rhs
}

let arr = [34, 99, 56, 12, 10, 108, 543, 7]
print("Original Array: \(arr)")
let sortedArr = arr.sorted(by: decreasingOrder)
print("Array sorted in Descending Order: \(sortedArr)")

Output 1:

Original Array: [34, 99, 56, 12, 10, 108, 543, 7]
Array sorted in Descending Order: [543, 108, 99, 56, 34, 12, 10, 7]

This looks too Verbose right ? We can simplify the above code using Closures:

Example 1.1:

let arr = [34, 99, 56, 12, 10, 108, 543, 7]
print("Original Array: \(arr)")
let sortedArr = arr.sorted(by: { (lhs: Int, rhs: Int) -> Bool in
    lhs > rhs
})
print("Array sorted in Descending Order: \(sortedArr)")

Output 1.1:

Original Array: [34, 99, 56, 12, 10, 108, 543, 7]
Array sorted in Descending Order: [543, 108, 99, 56, 34, 12, 10, 7]

Notice that just like Functions, Closures can also use implicit return (No need to specify return keyword when there is only a single expression in the body of closure).

A more simpler way is to pass the operator function > to the sorted(by:) function. But, it is not preferred here as we are discussing about Closures.

Inferring Type from Context

Since the sorted(by:) function takes a closure as an argument, the type of parameter and return type of the Closure can be inferred by Swift. Now, Example 1 can be modified as shown below:

Example 1.2:

let arr = [34, 99, 56, 12, 10, 108, 543, 7]
print("Original Array: \(arr)")
let sortedArr = arr.sorted(by: { lhs, rhs in lhs > rhs })
print("Array sorted in Descending Order: \(sortedArr)")

Output 1.2:

Original Array: [34, 99, 56, 12, 10, 108, 543, 7]
Array sorted in Descending Order: [543, 108, 99, 56, 34, 12, 10, 7]

Shorthand Argument Names

Swift automatically provides shorthand argument names to inline closures, which can be used to refer to the values of the closure’s arguments by the names $0, $1, $2 (upto n-1 where n is the number of arguments), and so on. By doing so, the in keyword and argument list can be avoided.

Example 1.3:

let arr = [34, 99, 56, 12, 10, 108, 543, 7]
print("Original Array: \(arr)")
let sortedArr = arr.sorted(by: { $0 > $1 })
print("Array sorted in Descending Order: \(sortedArr)")

Output 1.3:

Original Array: [34, 99, 56, 12, 10, 108, 543, 7]
Array sorted in Descending Order: [543, 108, 99, 56, 34, 12, 10, 7]

Trailing Closures

If you need to pass a closure expression to a function as the function’s final argument and the closure expression is long, it can be useful to write it as a trailing closure instead. You write a trailing closure after the function call’s parentheses, even though the trailing closure is still an argument to the function. When you use the trailing closure syntax, you don’t write the argument label for the first closure as part of the function call. A function call can even include multiple trailing closures.

Now, Example 1.3 can be modified as:

Example 1.4:

let arr = [34, 99, 56, 12, 10, 108, 543, 7]
print("Original Array: \(arr)")
let sortedArr = arr.sorted() { $0 > $1 }
print("Array sorted in Descending Order: \(sortedArr)")

Output 1.4:

Original Array: [34, 99, 56, 12, 10, 108, 543, 7]
Array sorted in Descending Order: [543, 108, 99, 56, 34, 12, 10, 7]

Now, since the closure is the only argument provided to the function, the code can be further simplified as:

Example 1.5:

let arr = [34, 99, 56, 12, 10, 108, 543, 7]
print("Original Array: \(arr)")
let sortedArr = arr.sorted { $0 > $1 }
print("Array sorted in Descending Order: \(sortedArr)")

Output 1.5:

Original Array: [34, 99, 56, 12, 10, 108, 543, 7]
Array sorted in Descending Order: [543, 108, 99, 56, 34, 12, 10, 7]

Notice how using Closure simplified the large expression in Example 1 to a single line Expression in Example 1.5.

If a function takes multiple closures, you omit the argument label for the first trailing closure and you label the remaining trailing closures. For example, the function below loads a picture for a photo gallery:

Example 2:

func loadPicture(from server: Server, completion: (Picture) -> Void, onFailure: () -> Void) {
    if let picture = download("photo.jpg", from: server) {
        completion(picture)
    } else {
        onFailure()
    }
}

When you call this function to load a picture, you provide two closures. The first closure is a completion handler that displays a picture after a successful download. The second closure is an error handler that displays an error to the user. Now, to call the loadPicture(from:completion:onFailure:), we can write the following code.

Example 2.1:

loadPicture(from: someServer) { picture in
    someView.currentPicture = picture
} onFailure: {
    print("Couldn't download the next picture.")
}

In this example, the loadPicture(from:completion:onFailure:) function dispatches its network task into the background, and calls one of the two completion handlers when the network task finishes. Writing the function this way lets you cleanly separate the code that’s responsible for handling a network failure from the code that updates the user interface after a successful download, instead of using just one closure that handles both circumstances.

Capturing Values

A closure can capture constants and variables from the surrounding context in which it’s defined. The closure can then refer to and modify the values of those constants and variables from within its body, even if the original scope that defined the constants and variables no longer exists.

In Swift, the simplest form of a closure that can capture values is a nested function, written within the body of another function. A nested function can capture any of its outer function’s arguments and can also capture any constants and variables defined within the outer function.

Example 3:

func makeIncrementer(forIncrement amount: Int) -> () -> Int
{
    var runningTotal = 0
    func incrementer() -> Int
    {
        runningTotal += amount // runningTotal and amount captured by closure
        return runningTotal
    }
    return incrementer
}
let incrementBy10 = makeIncrementer(forIncrement: 10)
for _ in 0...3
{
    print("Incremented by 10: \(incrementBy10())")
}
let incrementBy20 = makeIncrementer(forIncrement: 20)
for _ in 0...3
{
    print("Incremented by 20: \(incrementBy20())")
}
print("Sum of both incrementers: \(incrementBy10() + incrementBy20())")

Output 3:

Incremented by 10: 10
Incremented by 10: 20
Incremented by 10: 30
Incremented by 10: 40
Incremented by 20: 20
Incremented by 20: 40
Incremented by 20: 60
Incremented by 20: 80
Sum of both incrementers: 150

The return type of makeIncrementer(forIncrement:) is () -> Int. This means that it returns a function, rather than a simple value. The function it returns has no parameters, and returns an Int value each time it’s called. The makeIncrementer(forIncrement:) function defines an integer variable called runningTotal, to store the current running total of the incrementer that will be returned. This variable is initialized with a value of 0. The makeIncrementer(forIncrement:) function has a single Int parameter with an argument label of forIncrement, and a parameter name of amount. The argument value passed to this parameter specifies how much runningTotal should be incremented by each time the returned incrementer function is called. The makeIncrementer(forIncrement:) function defines a nested function called incrementer, which performs the actual incrementing. This function simply adds amount to runningTotal, and returns the result.

The incrementer() function doesn’t have any parameters, and yet it refers to runningTotal and amount from within its function body. It does this by capturing a reference to runningTotal and amount from the surrounding function and using them within its own function body. Capturing by reference ensures that runningTotal and amount don’t disappear when the call to makeIncrementer(forIncrement:) ends, and also ensures that runningTotal is available the next time the incrementer() function is called.

NOTE: If you assign a closure to a property of a class instance, and the closure captures that instance by referring to the instance or its members, you will create a strong reference cycle between the closure and the instance. Swift uses Capture Lists to break these strong reference cycles. By means of Capture Lists, we can specify whether the captured references should be weak or unowned references, thereby preventing Strong Reference Cycles. Capture Lists can be defined before closure parameters within square brackets [] separated by commas ,.

Closures are Reference Types

In the example above, incrementBy10 and incrementBy20 are constants, but the closures these constants refer to are still able to increment the runningTotal variables that they have captured. This is because functions and closures are reference types.

Whenever you assign a function or a closure to a constant or a variable, you are actually setting that constant or variable to be a reference to the function or closure. In the example above, it’s the choice of closure that incrementBy10 refers to that’s constant, and not the contents of the closure itself. This also means that if you assign a closure to two different constants or variables, both of those constants or variables refer to the same closure.

Escaping Closures

A Closure is called as an Escaping Closure when it is passed as an argument to a function but is called only after the function execution completes. Escaping Closures are denoted by placing @escaping attribute (Attributes in Swift are like Annotations in Java) before the closure type. Escaping Closures can be used to handle events like completion of a function.

Example 4:

var completionHandlers: [Int : (Int) -> Void] = [:]
func functionWithCompletionTracker(seed: Int, completionHandler: @escaping (Int) -> Void)
{
    if seed.isMultiple(of: 2)
    {
        completionHandlers[seed] = completionHandler
    }
}
let sampleCompletionHandler: (Int) -> Void  = {
    print("functionWithCompletionTracker() completed successfully for seed \($0) !")
}
for x in 0..<4
{
    let random = Int.random(in: x...40)
    functionWithCompletionTracker(seed: random, completionHandler: sampleCompletionHandler)
    if let handler = completionHandlers[random]
    {
        handler(random)
    }
    else
    {
        print("No Completion Handler for \(random) !")
    }
}

Output 4:

No Completion Handler for 15 !
No Completion Handler for 27 !
functionWithCompletionTracker() completed successfully for seed 10 !
functionWithCompletionTracker() completed successfully for seed 10 !

Normally, a closure captures variables implicitly by using them in the body of the closure, but in this case you need to be explicit. If you want to capture self, write self explicitly when you use it, or include self in the closure’s capture list. Writing self explicitly lets you express your intent, and reminds you to confirm that there isn’t a reference cycle. If self is an instance of a structure or an enumeration, you can always refer to self implicitly. However, an escaping closure can’t capture a mutable reference to self when self is an instance of a structure or an enumeration.

Autoclosures

An autoclosure is a closure that’s automatically created to wrap an expression that’s being passed as an argument to a function. It doesn’t take any arguments, and when it’s called, it returns the value of the expression that’s wrapped inside of it. This syntactic convenience lets you omit braces around a function’s parameter by writing a normal expression instead of an explicit closure.

An autoclosure lets you delay evaluation, because the code inside isn’t run until you call the closure. Delaying evaluation is useful for code that has side effects or is computationally expensive, because it lets you control when that code is evaluated. The code below shows how a closure delays evaluation.

Example 5:

var customersInLine = ["Kris", "Hari", "Shiv", "Sunder", "Sathya"]
print(customersInLine.count)
let customerProvider = { customersInLine.remove(at: 0) }
print(customersInLine.count)
print("Now serving \(customerProvider())!")
print(customersInLine.count)

Output 5:

5
5
Now serving Kris!
4

Even though the first element of the customersInLine array is removed by the code inside the closure, the array element isn’t removed until the closure is actually called. If the closure is never called, the expression inside the closure is never evaluated, which means the array element is never removed. Note that the type of customerProvider isn’t String but () -> String —a function with no parameters that returns a String.

Now, when I use an AutoClosure (denoted by @autoclosure attribute) as a function parameter, I can directly pass an expression for the closure instead of a closure.

Modified Example 5:

var customersInLine = ["Kris", "Hari", "Shiv", "Sunder", "Sathya"]
print(customersInLine.count)
func serveCustomer(customerProvider: @autoclosure () -> String)
{
    print("Now serving \(customerProvider())!")
}
serveCustomer(customerProvider: customersInLine.remove(at: 0)) // Using autoclosure
print(customersInLine.count)

Output 5:

5
Now serving Kris!
4

Now that we have seen about closures, let's move on to see about Structures in Swift.

← Back to Index
← Functions Structures →