---
title: Functions
source: 'https://academia.sh/en/courses/swift/functions'
course: 'The Swift Language'
language: en
updated: '2026-10-21T05:03:11+00:00'
license: 'CC BY-SA 4.0'
objectives:
    - 'Use argument labels, default values, tuple returns and `inout`'
    - 'Show that a default argument expression is evaluated on every call'
    - 'Explain that the default expression is never evaluated when an argument is given'
    - 'State that an argument is evaluated before the call even if the body does not read it'
---

# Functions

Introduces argument labels, default values, multiple returns through a tuple and the `inout` parameter; measures that a default argument expression is evaluated on every call.

In control flow, conditions were evaluated only when needed. In functions the question goes one step further: when is the argument of a call evaluated, and when is the default value of a parameter? A function in Swift is designed to look like a sentence the caller can read; the measurement in this lesson shows the evaluation order behind that sentence.

## Labels, Defaults and Return

In Swift every parameter has an argument label, and it is written at the call. In the definition `func move(from start: Int, to end: Int)`, `from` and `to` are the labels the caller sees, while `start` and `end` are the names the body uses. If you write an underscore instead of a label, the call has no label: `func pad(_ text: String)`. Adding `= value` to a parameter defines a default argument; if the caller does not give that parameter, the default expression is used.

A function can return a tuple to return several values: `-> (low: Int, high: Int)`. A labeled tuple is read on the caller's side as `result.low`. An `inout` parameter lets the function change the caller's variable; the variable is prefixed with `&` at the call and has to be a `var`.

```swift
// Probe.swift — records when an expression is evaluated
final class Probe {
    let label: String
    private(set) var evaluations = 0
    private(set) var uses = 0
    private var evaluatedBeforeUse = false

    init(_ label: String) {
        self.label = label
    }

    func eval<T>(_ value: T) -> T {
        evaluations += 1
        if uses == 0 { evaluatedBeforeUse = true }
        return value
    }

    func use<T>(_ access: () -> T) -> T {
        uses += 1
        return access()
    }

    var moment: String {
        if evaluations == 0 { return "never" }
        if evaluations > 1 { return "on-every-use" }
        return evaluatedBeforeUse ? "immediately" : "on-first-use"
    }

    func report() {
        print("\(label): \(evaluations) evaluations, \(uses) uses -> \(moment)")
    }
}

func tally(_ probes: [Probe]) {
    for moment in ["immediately", "on-first-use", "on-every-use", "never"] {
        let count = probes.filter { $0.moment == moment }.count
        print("\(moment): \(count)")
    }
}
```

The measurement block first runs these four features. Then it builds two functions with the same body: `pad` takes its default width from a `Probe` when the caller does not give one, while `padGiven` has the same default but we give the width ourselves on every call. The functions are marked `@MainActor`, because the default expression reads the top-level `Probe`, and under the Swift 6 rules a `Probe` can be read only from the main actor.

```swift
// main.swift — label, default, tuple and inout
func move(from start: Int, to end: Int) -> Int { end - start }
func bounds(of values: [Int]) -> (low: Int, high: Int) {
    (values.min() ?? 0, values.max() ?? 0)
}
func increment(_ value: inout Int) { value += 1 }

print(move(from: 2, to: 5))
let found = bounds(of: [3, 1, 2])
print(found.low, found.high)
var total = 0
increment(&total)
increment(&total)
print(total)

let defaultProbe = Probe("default-expression")
let givenProbe = Probe("default-when-given")

@MainActor func pad(_ text: String, width: Int = defaultProbe.eval(6)) -> String {
    String(repeating: "*", count: width) + text
}

@MainActor func padGiven(_ text: String, width: Int = givenProbe.eval(6)) -> String {
    String(repeating: "*", count: width) + text
}

print(defaultProbe.use { pad("a") })
print(defaultProbe.use { pad("b") })
print(givenProbe.use { padGiven("c", width: 2) })
print(givenProbe.use { padGiven("d", width: 2) })

let allProbes = [defaultProbe, givenProbe]
for probe in allProbes { probe.report() }
tally(allProbes)
```

```
3
1 3
2
******a
******b
**c
**d
default-expression: 2 evaluations, 2 uses -> on-every-use
default-when-given: 0 evaluations, 2 uses -> never
immediately: 0
on-first-use: 0
on-every-use: 1
never: 1
```

The `move` call gave 3, the `bounds` tuple was read as `1 3`, and the two `increment` calls made `total` 2. The key lines of the measurement are at the end. `default-expression` was evaluated twice in two calls: a default expression is not computed once at the function definition and stored, it runs again on every call, and its result is `on-every-use`. `default-when-given` showed zero evaluations in two calls, because the caller gave the width itself and the default expression was never reached: `never`.

**LB46.** The default argument expression was measured in a `@MainActor` function and only for a constant integer; the same result for other types or in a `nonisolated` context is not a claim of this lesson.

**LB47.** Whether the default expression runs in the caller's context or the callee's context is not distinguished in this lesson; what was measured is how many times and when it was evaluated.

## Variadic Parameters

If you write three dots after the type of a parameter, the function takes as many arguments as you like at that point: `func sumAll(of numbers: Int...) -> Int`. Inside the function body `numbers` is an ordinary `[Int]` array. At the call you write the arguments separated by commas; giving no argument at all is allowed too, and then the array arrives empty. A function can have more than one variadic parameter; the parameter that comes after the first one has to carry an argument label.

Arguments are evaluated before the call; three arguments mean three expressions. The block below makes two calls that pass each argument through a `Probe`, and adds one call without arguments.

```swift
// Variadic.swift — variadic parameters
func sumAll(of numbers: Int...) -> Int {
    numbers.reduce(0, +)
}
let variadicProbe = Probe("variadic-count")
print(variadicProbe.use {
    sumAll(of: variadicProbe.eval(1), variadicProbe.eval(2), variadicProbe.eval(3))
})
print(variadicProbe.use {
    sumAll(of: variadicProbe.eval(4), variadicProbe.eval(5))
})
print(sumAll())
variadicProbe.report()
```

```
6
9
0
variadic-count: 5 evaluations, 2 uses -> on-every-use
```

The sum of three numbers gave 6, the sum of two numbers 9, and the call without arguments 0. The `Probe` showed five evaluations: three in the first call and two in the second. Each argument is its own expression and is computed before the call; the function body receives it ready as an array. So the moment reads `on-every-use`.

A variadic parameter simplifies the caller's side, because no array has to be built. The number of arguments changes with the call; the measurement shows that this change also changes the number of evaluations.

**LB48.** The variadic parameter was measured with `Int`; Swift has no way to spread an array into a variadic parameter; if an array is needed, the parameter is declared as `[Int]`.

## A Nested Function Captures Its Surroundings

You can define another function inside a function. The inner function sees the local variables and parameters of the outer function and captures them. If the outer function returns the inner function, the captured variables go on living after the outer function has finished. This is the way to build a counter that carries its own state on every call.

The block below writes a function named `makeCounter`. The function takes a start value; the inner function `advance` increases the number by one on every call, passes it through a `Probe`, and the function returns the inner function. Two independent counters are built: the first is called twice, the second once.

```swift
// Counter.swift — a nested function carries state
func makeCounter(start: Int, probe: Probe) -> () -> Int {
    var current = start
    func advance() -> Int {
        current += 1
        return probe.eval(current)
    }
    return advance
}
let nestedProbe = Probe("nested-function")
let first = makeCounter(start: 10, probe: nestedProbe)
let second = makeCounter(start: 100, probe: nestedProbe)
print(nestedProbe.use { first() })
print(nestedProbe.use { first() })
print(nestedProbe.use { second() })
nestedProbe.report()
```

```
11
12
101
nested-function: 3 evaluations, 3 uses -> on-every-use
```

The first counter gave 11 and 12, the second counter 101. Although the two counters came out of the same function, each captured a separate `current`; while one advanced, the other was not affected. Three calls brought three evaluations, and the moment is `on-every-use`. Even after `makeCounter` has finished, `current` is alive, because the inner function holds it.

This prepares the topic of the next lesson: a nested function is a closure that has been given a name. The capture rules are the same.

**LB49.** The nested function captured an `Int` state; cases where the captured value is a class instance are outside this lesson.

## inout Writes Back

An `inout` parameter lets the function change the caller's variable. The mechanism is "copy, change, write back": the function takes a copy of the variable, changes it as much as it wants in the body, and when the function ends it writes the result back to the variable in one go. If the variable has an observer (`didSet`), the observer runs once at the write-back, not at each change in the body. A direct assignment, on the other hand, runs the observer on every line.

The block below does the same job in two ways: one increments twice with a function that has an `inout` parameter, the other writes the two increments directly. Both variables have a `didSet` observer, and each observer writes to its own `Probe`. Both ways are run for two rounds.

```swift
// Inout.swift — inout write-back
func addTwice(_ value: inout Int) {
    value += 1
    value += 1
}
let viaCallProbe = Probe("inout-observer")
let directProbe = Probe("direct-observer")
var viaCall = 0 { didSet { _ = viaCallProbe.eval(viaCall) } }
var direct = 0 { didSet { _ = directProbe.eval(direct) } }
_ = viaCallProbe.use { addTwice(&viaCall) }
_ = viaCallProbe.use { addTwice(&viaCall) }
_ = directProbe.use { direct += 1; direct += 1 }
_ = directProbe.use { direct += 1; direct += 1 }
print(viaCall, direct)
viaCallProbe.report()
directProbe.report()
```

```
4 4
inout-observer: 2 evaluations, 2 uses -> on-every-use
direct-observer: 4 evaluations, 2 uses -> on-every-use
```

Both variables became 4, but the observer counts diverged: the `inout` way showed two evaluations, the direct way four evaluations. `addTwice` incremented twice in the body; the observer ran at the write-back, once per call. In the direct way each `+=` triggered its own observer. Both lines read `on-every-use`, but their counts differ.

This difference needs care when `inout` and an observer come together: there may be intermediate values the observer does not see. If you want to see the intermediate states, write directly; if you want only the result, `inout` is enough.

**LB50.** The observer count was measured with a top-level variable; that `inout` write-back gives the same count on class properties and struct properties is outside this lesson.

## Function Type and Function as a Value

Every function has a type: the combination of the parameter types and the return type. The type of the function `func triple(_ n: Int) -> Int` is written `(Int) -> Int`. This type is used like an ordinary type: you can assign it to a constant, write it as a parameter type, or return it from another function. Assigning a function to a constant does not call it; it only carries it as a value. A call is made only with parentheses.

The block below assigns the `triple` function to a constant, prints its type, and passes it as an argument to another function named `apply`. `apply` applies the given function to a value. The value to apply comes from a `Probe`, and `apply` is called twice.

```swift
// FunctionType.swift — function type
func triple(_ n: Int) -> Int { n * 3 }
func apply(_ transform: (Int) -> Int, to value: Int) -> Int {
    transform(value)
}
let typeProbe = Probe("function-type")
let transform = triple
print(type(of: transform))
print(typeProbe.use { apply(transform, to: typeProbe.eval(4)) })
print(typeProbe.use { apply(transform, to: typeProbe.eval(5)) })
typeProbe.report()
```

```
@Sendable (Int) -> Int
12
15
function-type: 2 evaluations, 2 uses -> on-every-use
```

`type(of: transform)` printed `@Sendable (Int) -> Int`: besides the parameter and return types, the type carries a `@Sendable` attribute, because top-level functions can be shared safely in concurrent environments. In reading, the `(Int) -> Int` part is the decisive one. `apply` gave `12` and `15` in two calls: `4 * 3` and `5 * 3`. The `Probe` showed two evaluations; in each call the argument was computed once, before `apply` ran. The `transform` constant itself was not counted as an evaluation, because assigning a function to a constant does not run it.

When you carry a function as a value, the moment of calling stays with you: when you call it, how many times and with which argument is clear at the call site.

Once you have learned to read a function type, the signatures in the standard library become readable too. The `map` method of an array expects an `(Element) -> T` function; you can give it a named function or a closure. The closure you will see in the next lesson is a function value without a name and obeys the same type rules.

The `@Sendable` attribute at the start of the type can be ignored for now; you will see it with concurrency in later topics. What matters is knowing that a function is carried as a value and does not run until the call parentheses arrive. Assigning the same function to two different variables does not produce two separate copies; both point to the same function.

What is computed before the call is the argument, not the function value; the two evaluations of the measurement confirm this.

**LB51.** The function type was measured with a function that has one parameter; functions with several parameters and functions that return functions are outside this lesson.

## The Limiting Measurement

A default value comes into play when the caller does not give one. When the caller gives an argument, the opposite holds: that expression is evaluated before the call, whether the function body reads it or not. This is an ordinary rule for the language, but before the measurement shows it, an intuition lingers: the argument of a parameter that is not read seems never to be evaluated. The body below never reads its parameter, and the helper function `marked` shows with `print` when the argument ran.

```swift
// main.swift — the argument is evaluated before the body
func marked(_ n: Int) -> Int {
    print("argument \(n)")
    return n
}

func ignore(_ value: Int) -> String {
    print("body ran")
    return "did not read"
}

let argumentProbe = Probe("argument-expression")
_ = argumentProbe.use { ignore(argumentProbe.eval(marked(7))) }
_ = argumentProbe.use { ignore(argumentProbe.eval(marked(8))) }

let allProbes = [argumentProbe]
for probe in allProbes { probe.report() }
tally(allProbes)
```

```
argument 7
body ran
argument 8
body ran
argument-expression: 2 evaluations, 2 uses -> on-every-use
immediately: 0
on-first-use: 0
on-every-use: 1
never: 0
```

The line `argument 7` was printed before the line `body ran`, even though the body never used the `value` parameter. `argument-expression` was evaluated twice in two calls: the argument is computed every time, before the function is called. The difference from the default expression is this: a default expression runs only when no argument is given, while a given argument always runs.

**LB52.** The fact that the `ignore` body does not read the parameter does not measure whether the compiler eliminates the argument expression; the `argument 7` line in the output is the only evidence of this measurement and holds only for this construct.

## Summary

- The argument label makes the call readable; the `_` label removes it, and `= value` defines a default argument.
- A tuple returns several values; an `inout` parameter changes the caller's `var` variable through `&`.
- The default argument expression is evaluated on every call in which no argument is given.
- If an argument is given, the default expression is never evaluated; a given argument is evaluated before the call even if the body does not read it.
- The moment of this lesson: **on every use** (the default argument expression).

## Next Step

A function can take another function as a parameter, and you can write it as an expression without a name. When does the body of this expression run, and at which moment does it see the variables around it? The next lesson looks at closures.
