Functions

A function packages a computation behind a name so it can be reused, tested, and reasoned about in isolation. Swift adds a feature many languages lack: every parameter can carry an argument label, so call sites read like sentences.

This lesson moves from the basic shape of a function to the parameter modifiers that control how values flow in and out, and finally treats functions as values in their own right.

Function Basics

A function declares a name, a parameter list, and — when it produces a result — a return type after the arrow. The parameter list is what makes Swift call sites self-documenting.

Declaring & Calling Functions

The func keyword starts the declaration. A function that returns nothing may omit the arrow entirely, and a body holding a single expression may omit return.

// No parameters, no return value
func sayHello() {
    print("Hello!")
}
sayHello()                          // prints: Hello!

// One parameter, and a return type after the arrow
func square(_ value: Int) -> Int {
    return value * value
}
print(square(5))                    // prints: 25

// A single-expression body may drop the `return` keyword
func cube(_ value: Int) -> Int { value * value * value }
print(cube(3))                      // prints: 27

// `_` as the label lets the caller pass values positionally
func add(_ a: Int, _ b: Int) -> Int { a + b }
print(add(2, 3))                    // prints: 5

Parameters & Argument Labels

Every parameter has two names: an external label used at the call site and an internal name used inside the body. Design the label so the call reads as a phrase; that is why Swift's own APIs say power(number:times:) rather than power(_:_:).

// `number`/`times` are the external labels; `base`/`exponent` are internal
func power(number base: Int, times exponent: Int) -> Int {
    var result = 1
    for _ in 0..<exponent { result *= base }
    return result
}
print(power(number: 2, times: 10))   // 1024 — the call reads as a sentence

// Omitting the label entirely: write `_` instead of a name
func greet(_ name: String) -> String { "Hi, \(name)" }
print(greet("Ada"))                  // prints: Hi, Ada

Return Values & Tuples

A function may return a single value, several values bundled in a tuple, or nothing at all. When "no result" is a legitimate outcome, returning an optional is the idiomatic way to say so.

// Multiple results come back as one named tuple
func minMax(_ values: [Int]) -> (min: Int, max: Int)? {
    guard let first = values.first else { return nil }   // empty input
    var low = first
    var high = first
    for value in values {
        low = Swift.min(low, value)
        high = Swift.max(high, value)
    }
    return (low, high)
}

if let range = minMax([3, 1, 9, 4]) {
    print(range.min, range.max)     // prints: 1 9 — accessed by name
    print(range.0, range.1)         // prints: 1 9 — or by position
}
print(minMax([]) == nil)            // prints: true — empty input, no result

Advanced Parameters

Swift can vary the number of arguments, hand a parameter direct access to the caller's storage, and supply defaults so a single function serves several call shapes without needing overloads.

Variadic Parameters

A ... after the type accepts zero or more values of that type. Inside the body the parameter is an ordinary array, so you can loop or reduce over it. Only one variadic parameter is allowed per function.

func total(_ numbers: Int...) -> Int {
    numbers.reduce(0, +)          // inside the body `numbers` is [Int]
}
print(total(1, 2, 3))             // prints: 6
print(total())                    // prints: 0 — zero arguments are legal

func log(_ level: String, _ messages: String...) {
    for message in messages { print("[\(level)] \(message)") }
}
log("INFO", "started", "ready")   // prints two labelled lines

inout Parameters

Value types are copied into a function, so the body cannot change the caller's variable — unless the parameter is marked inout. That promises to modify the caller's storage in place, and the call site must acknowledge it with an ampersand.

func double(_ value: inout Int) {
    value *= 2                    // modifies the caller's variable
}

var counter = 5
double(&counter)
print(counter)                    // prints: 10

// inout is also the idiomatic way to exchange two values
var a = 1, b = 2
swap(&a, &b)
print(a, b)                       // prints: 2 1

Default Values

A parameter with a default value may be left out by the caller. Defaults usually sit at the end of the list, which lets the same function serve a short and a detailed call without any repetition.

func greet(_ name: String, greeting: String = "Hello") -> String {
    "\(greeting), \(name)!"
}
print(greet("Ada"))                    // prints: Hello, Ada!
print(greet("Ada", greeting: "Hi"))    // prints: Hi, Ada!

// A defaulted count plus a closure keeps one function usable in two shapes
func repeatTask(times: Int = 1, action: () -> Void) {
    for _ in 0..<times { action() }
}
repeatTask { print("run") }            // prints: run
repeatTask(times: 2) { print("run") }  // prints: run, twice

Functions as Values

A function has a type, written as its parameter types followed by its return type. That single fact means a function can be stored in a variable, passed as an argument, returned from another function, and selected at runtime — exactly like any other value.

Function Types

The type (Int, Int) -> Int reads as "takes two Ints and returns an Int". Referring to a function by name, without parentheses, produces that value rather than calling it.

// A variable whose type is "takes two Ints, returns an Int"
let operation: (Int, Int) -> Int = { $0 + $1 }
print(operation(4, 5))              // prints: 9

// A named function used as a value — no parentheses here
func subtract(_ a: Int, _ b: Int) -> Int { a - b }
let chosen: (Int, Int) -> Int = subtract
print(chosen(9, 4))                 // prints: 5

// A function that returns a function: pick the behaviour by name
func chooser(_ symbol: String) -> (Int, Int) -> Int {
    switch symbol {
    case "-": return subtract
    default:  return operation
    }
}
print(chooser("-")(10, 3))          // prints: 7 — call the returned function

Nested & Local Functions

A function declared inside another is visible only there, and it can use the enclosing function's parameters. That keeps a helper close to its single caller instead of polluting the global namespace.

func formatList(_ values: [Int], prefix: String) -> String {
    // Nested helper: it captures `prefix` from the enclosing scope
    func decorate(_ value: Int) -> String { "\(prefix)\(value)" }

    return values.map(decorate).joined(separator: ", ")
}
print(formatList([1, 2, 3], prefix: "#"))   // prints: #1, #2, #3

Overloading

Several functions may share one name as long as their parameter or return types differ; the compiler picks the right one from the argument types. Use distinct argument labels when the behaviours are genuinely different — overloads that silently change meaning are hard to review.

func render(_ value: Int) -> String { "int: \(value)" }
func render(_ value: Double) -> String { "double: \(value)" }
func render(_ value: Bool) -> String { "bool: \(value)" }

print(render(7))            // prints: int: 7
print(render(7.5))          // prints: double: 7.5
print(render(true))         // prints: bool: true

// A dictionary of functions dispatches by key without any switch
let handlers: [String: (Int) -> Int] = [
    "double": { $0 * 2 },
    "square": { $0 * $0 }
]
print(handlers["square"]?(6) ?? 0)   // prints: 36

Common Pitfalls

Argument Label Confusion

Read a parameter as two names: the external label the caller types, then the internal name the body uses. Nothing in the declaration is accidental — the label exists to make the call site readable.

// Left to right: external label first, then the internal name
func move(from start: Int, to end: Int) -> Int {
    end - start                  // inside the body: `start` and `end`
}
print(move(from: 10, to: 25))    // prints: 15 — call site reads as a phrase

// `_` removes the label so the argument is positional
func moveTo(_ position: Int) -> String { "at \(position)" }
print(moveTo(25))                // prints: at 25

// The label pairs with the function name: scale(by:)
func scale(by factor: Double) -> String { "x\(factor)" }
print(scale(by: 2.5))            // prints: x2.5

Recursion & Base Cases

A recursive function calls itself, so it needs a base case that returns without recursing — and each step must move toward it. Recursion that never bottoms out, or that goes thousands of levels deep, overflows the call stack.

func factorial(_ n: Int) -> Int {
    guard n > 1 else { return 1 }   // base case stops the recursion
    return n * factorial(n - 1)     // the argument shrinks each call
}
print(factorial(5))                 // prints: 120
print(factorial(0))                 // prints: 1 — base case reached at once

// The same shape written as a single expression
func sumUpTo(_ n: Int) -> Int {
    n <= 0 ? 0 : n + sumUpTo(n - 1)
}
print(sumUpTo(4))                   // prints: 10

Functions are now first-class values in your toolkit: you can pass behaviour around, capture state, and choose an implementation at runtime. Next, handle the case where a value may be absent in Optionals.