Skip to content
Theory · Closures · lesson 1 of 8

Functions as values

What you'll learn

What you'll learn:

  • store a function in a variable, pass it as an argument and return it from another function;
  • write closures briefly: without types, with $0 and after the call's parentheses;
  • process arrays with the map, filter, reduce and compactMap methods;
  • explain how a closure captures a variable and why two constants can share one counter.
A recipe card { } with a clipped note count 3 going into a machine map, with jars coming out

Picture a recipe card. You can cook from it right now. Or you can hand the card to a friend, put it in a box of recipes or stick it on the fridge. Whoever ends up holding it cooks from it, whenever they need to.

In the functions theme you called a function at once: you wrote its name, the parentheses and the arguments. But a function can also be treated like a card. The function's name without parentheses is the card itself, and the parentheses after the name mean "cook it now".

Here is a function stored in a variable:

SWIFT
func double(_ x: Int) -> Int {
    x * 2
}

func square(_ x: Int) -> Int {
    x * x
}

var transform: (Int) -> Int = double
print(transform(5)) // 10
transform = square
print(transform(5)) // 25

Let's go through the program step by step:

  1. The functions double and square take an Int and return an Int.
  2. var transform: (Int) -> Int = double puts the function double itself into the variable. There are no parentheses after double, so the function is not called.
  3. transform(5) calls whatever the variable holds and prints 10.
  4. transform = square puts another function into the same variable.
  5. The same call transform(5) now prints 25.

A function type

Every value in Swift has a type, and so does a function. A function type is written like this: the parameter types in parentheses, an arrow and the result type. (Int) -> Int has three parts:

  • (Int) — the function takes one integer;
  • -> — the arrow, as in a function's header;
  • Int — the function returns an integer.

double and square do different things, but they have the same type, so both fit into transform. Here are three more types:

  • (Int, Int) -> Int — the function takes two numbers and returns a number;
  • (String) -> Void — the function takes a string and returns nothing;
  • () -> Int — the function takes no parameters and returns a number.

Argument labels are not part of the type. If you put the function add(a:b:) into a constant op of type (Int, Int) -> Int, you call it without labels: op(2, 3). The call op(a: 2, b: 3) does not compile.

A variable holds only a function of a matching type. A function that takes a string does not fit into transform: the compiler reports that the types do not match.

A function as a parameter

Since a function is a value, you can pass it to another function as an argument. It is like handing the card to a friend: they cook from your recipe as many times as they need.

SWIFT
func applyTwice(_ f: (Int) -> Int, to x: Int) -> Int {
    f(f(x))
}

print(applyTwice(double, to: 3)) // 12
print(applyTwice(square, to: 3)) // 81

Let's go through the first call:

  1. applyTwice receives the function double in the parameter f and the number 3 in the parameter x.
  2. The inner call f(x) is double(3), which gives 6.
  3. The outer call f(6) gives 12, and the program prints it.

In the second call f holds square: first 3 * 3, then 9 * 9, that is 81.

A function that takes another function or returns one is called a higher-order function. Two parties share the work here: applyTwice decides how many times and with which number to call f, and the function passed in decides what to do with that number.

A function as a result

A function can also be returned from another function. Then the header has a function type after the arrow:

SWIFT
func pick(big: Bool) -> (Int) -> Int {
    if big {
        return square
    }
    return double
}

let chosen = pick(big: true)
print(chosen(4)) // 16
print(pick(big: false)(4)) // 8

The header func pick(big: Bool) -> (Int) -> Int reads like this: the function pick takes a Bool and returns a function of type (Int) -> Int. Let's go through the calls:

  1. pick(big: true) returns not a number but the function square, and it goes into the constant chosen.
  2. chosen(4) calls square and prints 16.
  3. pick(big: false)(4) first gets the function double, and the second pair of parentheses calls it with the number 4 right away. The result is 8.

Interviewers often say it like this: in Swift, functions are first-class values. It means you can do with a function what you do with a number or a string:

  • put it into a variable or a constant;
  • pass it as an argument to another function;
  • return it from a function.

Here the recipe-card picture stops working. Once you hand over a paper card, you no longer have it. A function can sit in ten variables at once, and they all lead to the same function.

Common mistake

The function was passed with parentheses:

SWIFT
print(applyTwice(double(3), to: 3)) // ✗
TEXT
error: cannot convert value of type 'Int' to expected argument type '(Int) -> Int'

The program does not compile. The parentheses after double call the function on the spot, and double(3) gives the number 6. But the parameter f expects a function of type (Int) -> Int, not an Int. It is like handing your friend not the card but a dish that is already cooked. Pass the name without parentheses: applyTwice(double, to: 3).

Key points

  • A function is a value: you store it in a variable, pass it as an argument and return it from another function.
  • A function type is written as (Int) -> Int: the parameter types in parentheses, an arrow and the result type; argument labels are not part of the type.
  • A higher-order function takes another function or returns one.
  • A name without parentheses is the function itself; a name with parentheses is its call and its result.