Functions
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.