Swift: Function vs Closure
Video: Swift: Function vs Closure by Taught by Celeste AI - AI Coding Coach
Swift with Copilot: Function vs Closure
A function is a named, declared callable:
func add(_ a: Int, _ b: Int) -> Int { a + b }. A closure is an anonymous (or assigned) one:let add = { (a: Int, b: Int) in a + b }. Both can be passed around. Closures shine for short, inline transforms.
Same job, two syntaxes. When to use which is mostly readability.
Function
func add(_ a: Int, _ b: Int) -> Int {
return a + b
}
print(add(3, 5)) // 8
func name(args) -> ReturnType { body }. Named, declared at the top level (or inside a type). Can be referenced by name.
For single-expression bodies, the return is implicit:
func add(_ a: Int, _ b: Int) -> Int {
a + b
}
The _ before parameter names suppresses the argument label at the call site (add(3, 5) instead of add(a: 3, b: 5)).
Closure
let add: (Int, Int) -> Int = { (a, b) in
return a + b
}
print(add(3, 5)) // 8
{ (params) -> ReturnType in body }. Anonymous; assigned to a variable. The type annotation (Int, Int) -> Int lets Swift infer parameter types.
Compact form:
let add: (Int, Int) -> Int = { $0 + $1 }
$0, $1 are positional parameters. No in needed when types are inferred and you use $N.
Side by side
// Function form
func double(_ x: Int) -> Int {
x * 2
}
// Closure form (named)
let double: (Int) -> Int = { $0 * 2 }
// Inline closure (most common)
[1, 2, 3].map { $0 * 2 } // [2, 4, 6]
For a one-line transform passed to map, the inline closure is the cleanest. For a reusable, named operation, a function is conventional.
Both are first-class
In Swift, both functions and closures are values. You can:
- Assign to a variable.
- Pass as arguments.
- Return from another function.
- Store in arrays/dicts.
func double(_ x: Int) -> Int { x * 2 }
let triple = { (x: Int) -> Int in x * 3 }
let ops: [(Int) -> Int] = [double, triple]
ops.map { $0(5) } // [10, 15]
let values = [1, 2, 3].map(double)
// [2, 4, 6]
map(double) passes the function as the closure argument. No special syntax needed.
Closure capture
Closures capture variables from their surrounding scope:
func makeCounter() -> () -> Int {
var count = 0
return {
count += 1
return count
}
}
let counter = makeCounter()
print(counter()) // 1
print(counter()) // 2
print(counter()) // 3
The closure captures count by reference. Each call modifies it. Functions can do the same with closures inside, but the capture is what makes closures useful for state-bearing callbacks.
When to use which
Function: - Reusable operation with a clear name. - Public API. - Documentation/discoverability matters. - You'll write tests for it.
Closure:
- One-shot transform passed to map/filter/reduce.
- Callback for an async operation.
- Short logic that's clearer inline than at a distance.
- Capturing local state.
// Function — clear, reusable
func isEven(_ n: Int) -> Bool { n % 2 == 0 }
[1, 2, 3, 4].filter(isEven)
// Closure — inline, no need to name
[1, 2, 3, 4].filter { $0 % 2 == 0 }
For public APIs, prefer named functions. For internal pipelines, closures are fine.
Trailing closure syntax
When the last parameter is a closure, you can put it after the parens:
// All three are equivalent
arr.map({ $0 * 2 })
arr.map() { $0 * 2 }
arr.map { $0 * 2 }
Trailing closures are the Swift idiom — cleaner for multi-line:
let result = arr.reduce(into: [String: Int]()) { dict, item in
dict[item.key, default: 0] += 1
}
For multiple trailing closures (Swift 5.3+):
view.animate {
// animation
} completion: {
// after
}
Each labeled by parameter name except the first.
Function types
Both functions and closures share the same type system:
let f1: (Int, Int) -> Int = { $0 + $1 }
let f2: (Int, Int) -> Int = max(_:_:) // function reference
func apply(_ op: (Int, Int) -> Int, to a: Int, and b: Int) -> Int {
op(a, b)
}
apply(+, to: 3, and: 5) // 8
apply(max, to: 3, and: 5) // 5 — max is also a function
+, max, custom functions, closures — all interchangeable as (Int, Int) -> Int.
Closure types and @escaping
Closures stored or passed to async work need @escaping:
var handlers: [() -> Void] = []
func register(_ handler: @escaping () -> Void) {
handlers.append(handler) // stored — outlives the function
}
Without @escaping, the closure must run synchronously inside the function. The default is non-escaping (faster).
Capturing self in classes
class Network {
var pending: [() -> Void] = []
func fetch() {
URLSession.shared.dataTask(with: url) { [weak self] data, _, _ in
self?.handle(data)
}.resume()
}
}
[weak self] is a capture list — captures self weakly to avoid retain cycles. Crucial pattern in iOS/macOS code.
Common stumbles
Closure types vs concrete. let f: (Int) -> Int = double works because double has matching type. But let g = double makes g a function reference, not a closure value — distinction usually doesn't matter.
Forgetting _ for unlabeled args. func add(a: Int, b: Int) requires add(a: 3, b: 5). Use _ to suppress: func add(_ a: Int, _ b: Int).
Trailing closure ambiguity. Sometimes the compiler can't tell which form you mean. Use parens.
@escaping required. If you store the closure or run it asynchronously, mark with @escaping. Compiler will tell you.
Retain cycles with closures + classes. Use [weak self] or [unowned self] in capture lists.
{ ... } vs () -> Void. A closure's type is () -> Void (or whatever signature). Don't confuse the literal with the type.
What's next
Episode 14: Add 2 numbers — function and closure. Concrete demonstration of both forms.
Recap
Function: func name(args) -> Type { body }. Closure: { (args) -> Type in body } or shorthand { $0 + $1 }. Both are first-class — pass, store, return. Closures capture variables; functions don't (except via inner closures). For inline map/filter, use closures. For named, reusable ops, use functions. Trailing closure syntax for last-arg closures. @escaping for stored/async closures. [weak self] to avoid retain cycles in classes.
Next episode: add two numbers, both forms.