Function Types and Closures
In Functions, you learned that you can create reusable units of code by declaring functions. You can pass data into and out of a function through its parameters and return value. In this chapter, you’ll learn how function types and closures let you pass code into and out of a function.
Function types
Swift is a strictly typed language, and functions are no exception. A function’s type includes its parameter and return types. For example:
func toFahrenheit(celsius: Double) -> Double {
celsius * 1.8 + 32
}
The type of this function is (Double) -> Double
because it takes a Double
and returns a Double
.
What about functions that don’t take any parameters or return a value? For example:
func printGreeting() {
print("Hello")
}
The type of this function is () -> Void
. Here’s what this means:
The parentheses indicate an empty parameter list. Like a function declaration, a function type must include parentheses around its parameter list, even when that list is empty.
Void
is a special type that means empty, or nothing. You use it as the return type for functions that don’t return a value:swiftfunc printGreeting() -> Void { print("Hello") }
As you can see, you can make
Void
an explicit return type. However, an implicit-> Void
is preferred.
You can use a function type like any other type. For example:
var conversion: (Double) -> Double
This code declares conversion
as a variable of type (Double) -> Double
. This variable can hold any function that takes a Double
and returns a Double
, such as toFahrenheit
:
conversion = toFahrenheit
You refer to a function by name when you assign it to a variable. You can also use the function’s full name, in case its base name is ambiguous:
conversion = toFahrenheit(celsius:)
Of course, type inference supports function types as well:
var task = printGreeting
Here, the compiler infers the type of task
as () -> Void
.
After you’ve declared a constant or variable of a function type, you can call it just like a function:
conversion(0)
task()
Note how the parentheses disambiguate between a function and a function call:
task
is a function.task()
is a call to this function.
Higher-order functions
A function can take other functions as parameters and even return a function. It then becomes a higher-order function. The type of a higher-order function includes other function types.
In the previous chapter, you learned that you can sort a dictionary using the sorted(by:)
method. This method returns the key/value pairs of the dictionary in a sorted array. To sort the grades
dictionary from the previous chapter, you’d call sorted(by:)
with an argument of the following type:
((key: String, value: Int), (key: String, value: Int)) -> Bool
You should recognize this type as a function that takes two key/value pairs and returns a Bool
. sorted(by:)
uses this function to compare two student/grade pairs and determine their order. If the comparison function returns true
, the first pair is sorted before the second pair; if it returns false
, the second pair is sorted first.
Here’s an example:
var grades = ["Alice": 20, "Bob": 7, "Charlie": 14]
func nameAscending(
_ pair1: (student: String, grade: Int),
_ pair2: (student: String, grade: Int)
) -> Bool {
pair1.student < pair2.student
}
func gradeDescending(
_ pair1: (student: String, grade: Int),
_ pair2: (student: String, grade: Int)
) -> Bool {
pair1.grade > pair2.grade
}
grades.sorted(by: nameAscending)
grades.sorted(by: gradeDescending)
This example declares two comparison functions, nameAscending
and gradeDescending
. You can pass either of these to sorted(by:)
, which then sorts accordingly.
Closures
Having to declare nameAscending
and gradeDescending
up front isn’t practical. You only use these functions as arguments to sorted(by:)
, so it would be better if you could declare them inline as part of the function call. Closures provide a way to do this.
A closure is a function without a name. You specify its parameters, return type, and implementation, and you assign it to a variable or use it as a function argument.
Here’s how you call sorted(by:)
with a closure that compares by grade:
grades.sorted(by: {
(
pair1: (student: String, grade: Int),
pair2: (student: String, grade: Int)
) -> Bool in
return pair1.grade > pair2.grade
})
Here’s the closure on its own:
{
(
pair1: (student: String, grade: Int),
pair2: (student: String, grade: Int)
) -> Bool in
return pair1.grade > pair2.grade
}
This closure is similar to the declaration of gradeDescending
with the following differences:
- The keyword
func
and function name are gone. - The opening brace moves to the front so that the closure is fully enclosed in braces.
- The keyword
in
separates the closure’s parameters and return type from its body, a task previously performed by the opening brace.
A fully specified closure is quite verbose. Fortunately, there are several ways you can simplify closures. The following sections explain these ways.
Inferred types
More often than not, the compiler can infer a closure’s parameter and return types. In the example above, the sorted(by:)
method requires a function with two parameters of type (key: String, value: Int)
and a return type of Bool
. The closure doesn’t have to repeat this type information:
grades.sorted(by: {
(pair1, pair2) in
return pair1.value > pair2.value
})
This closure only declares the names of its parameters. Their types, as well as the return type, are inferred from the type of by
.
Implicit return statement
If the body of a closure consists entirely of a return
statement, you can make this statement implicit and only specify the return value:
grades.sorted(by: {
(pair1, pair2) in
pair1.value > pair2.value
})
Shorthand parameter names
If a closure’s parameter names are of no importance, you can use shorthand names instead: $0
, $1
, and so on. You can then omit the parameter list and the keyword in
:
grades.sorted(by: { $0.value > $1.value })
However, you can only use shorthand names if the closure uses its last parameter. The following example is invalid:
grades.sorted(by: { $0.value > 10 })
This closure only has one parameter and therefore doesn’t match the type of by
, which has two parameters. In this case, a parameter list is required:
grades.sorted(by: {
(pair1, _) in
pair1.value > 10
})
Trailing closures
If the last argument in a function call is a closure, you can write that closure after the argument list and discard its label (by:
):
grades.sorted() {
$0.value > $1.value
}
You can even discard the parentheses if the closure is the only argument:
grades.sorted {
$0.value > $1.value
}
This syntax is known as trailing closure syntax. It may seem strange at first because it looks very different from a regular function call. However, trailing closures can be an elegant way to pass code to a function. They let you write a function argument in the same way you write the body of a control flow statement: as a block of code surrounded by braces.
A function call can have multiple trailing closures. For example, here’s a function that takes two function parameters:
func when(_ value: String?, then: (String) -> Void, otherwise: () -> Void) {
if let value, !value.isEmpty {
then(value)
} else {
otherwise()
}
}
Without trailing closures, you’d call this function as follows:
var input: String? = "Hello"
when(input, then: {
print($0)
}, otherwise: {
print("Input was empty.")
})
With trailing closures, this call becomes:
when(input) {
print($0)
} otherwise: {
print("Input was empty.")
}
As you can see, only the first trailing closure drops its argument label. All other trailing closures must be labeled.
Local variable capturing
Now that you know how to declare closures, let’s discuss an important aspect of their behavior.
Consider the following example:
func makeCounter() -> () -> Int {
var count = 0
return {
count += 1
return count
}
}
This function creates and returns a closure of type () -> Int
. This closure works like a counter: every time you call it, it increments and returns its value. Here’s how you use it:
var counter = makeCounter()
counter()
counter()
Note how this closure uses the local variable count
. This variable is scoped to makeCounter
, which means it’s out of scope by the time you call counter
. Despite this, counter
works fine, incrementing and returning count
, even though count
should no longer exist. Why is this?
When a closure uses a local variable, it captures this variable. A capture extends the lifetime of a variable beyond its normal scope, so it remains available to the closure that captured it. This process is known as local variable capturing.
Now consider what happens when you call makeCounter
a second time:
var secondCounter = makeCounter()
secondCounter()
Even though count
is being kept alive by counter
, a second call to makeCounter
creates a new local variable, as you would expect. This means counter
and secondCounter
have their own separate, captured count
variable.
Examples from the Standard Library
This section demonstrates some of the higher-order methods available on collection types. These methods leverage function types and are designed to be used with closures. All examples use simplified trailing closures.
contains
The contains
method checks if the collection contains at least one element that satisfies a given condition.
For example, you can use contains
to check if at least one student attained a perfect grade of 20:
grades.contains { $0.value == 20 }
The closure in this example is a function that takes an element of the collection and returns a Bool
. Such a function is known as a predicate.
Predicates are common in the Standard Library. They describe a characteristic a value may or may not have. In this case, the characteristic is “has a perfect grade”.
allSatisfy
The allSatisfy
method checks if all of the elements in the collection satisfy a given predicate. For example, you can use allSatisfy
to check if all students have a passing grade:
grades.allSatisfy { $0.value >= 10 }
filter
The filter
method takes a predicate and returns a new collection containing only the elements that match the predicate. The original collection is unaffected.
The following example uses filter
to create a dictionary of students with a passing grade:
grades.filter { $0.value >= 10 }
map
The map
method takes a transformation function and applies it to each element of the collection. It gathers the transformed elements in an array, which it returns. The original collection is unaffected.
The following example uses map
to build an array of strings that describe the contents of grades
:
grades.map { "\($0.key) has a grade of \($0.value)." }
Here, the transformation function is a closure that takes an element of the collection and returns a string. map
applies this function to every pair in the dictionary and returns the resulting strings.
Methods like filter
and map
are often chained together. In a method chain, one method is applied to the output of the previous method. For example:
grades.filter { $0.value >= 10 }.map { $0.key }
This code first creates a dictionary of students with a passing grade, then maps this dictionary to an array of students.
forEach
The forEach
method applies a function to every element of the collection. The goal of this function is solely to process an element; it doesn’t return anything, and neither does forEach
.
The following example prints the students that have a passing grade, sorted alphabetically:
grades.filter { $0.value >= 10 }
.map { $0.key }
.sorted()
.forEach { print($0) }
Here’s an equivalent example that uses a for-in
loop:
for student in grades.filter({ $0.value >= 10 })
.map({ $0.key })
.sorted() {
print(student)
}
As you can see, the forEach
method and the for-in
loop perform a similar task. However, there are a few differences:
- A
for-in
loop can’t use trailing closures in its condition because the opening brace of a trailing closure would be mistaken for the opening brace of the loop’s body. - You can’t use
break
with theforEach
method because there’s no loop statement to break. - You can’t use
continue
with theforEach
method, though you can skip elements with areturn
statement as this ends the processing function.
The forEach
method is designed for use in a method chain, and because it doesn’t return anything, it ends the chain.
reduce
The reduce
method processes the entire collection and reduces it to a single value. It takes two parameters:
- An input value for the first reduction step.
- A reduction function that takes an input value (
$0
) and a single element from the collection ($1
). This function should combine the element with the input value and return a new value that becomes the input for the next reduction step.
The reduce
method applies your reduction function to the collection, one element at a time, and returns the output of the final step. The collection itself is unaffected.
Here’s an example that uses reduce
to calculate the sum of the student’s grades:
grades.map { $0.value }.reduce(0) { $0 + $1 }
This code first maps the students to their grades, then uses reduce
to sum the grades. Assuming grades
has the value ["Alice": 20, "Bob": 7, "Charlie": 14]
, the reduction goes as follows:
- Step 1 has an input of 0 and processes the first element, 20:
0 + 20 = 20
- Step 2 has an input of 20 and processes the second element, 7:
20 + 7 = 27
- Step 3 has an input of 27 and processes the third element, 14:
27 + 14 = 41
Finally, reduce
returns the output of the last step: 41.
Note
The order of the steps may be different from what’s shown here because dictionaries are unordered. Nevertheless, the end result is the same.
In the previous example, there’s no need to write a closure for the reduction function. Swift already includes a function that returns the sum of two integers:
grades.map { $0.value }.reduce(0, +)
There’s no magic involved here. Operators are functions declared in the Standard Library. They get some special treatment, but other than that, they’re just functions.
Up next
This chapter has shown how higher-order functions let you pass code into and out of a function. This code can be either a closure, a function of your own, or a function provided by Swift, such as an operator.
It’s important that you get proficient at using closures; many methods from the Standard Library accept them. You’ll get a chance to practice using closures with the upcoming exercises.
Complete these exercises first, then continue on to your second challenge, where you’ll build a game of Blackjack.