Part of Swift with Copilot

Swift: Function vs Closure

Sandy LaneSandy Lane

Video: Swift: Function vs Closure by Taught by Celeste AI - AI Coding Coach

Take the quiz on the full lesson page
Test what you've read · interactive walkthrough

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.

Ready? Take the quiz on the full lesson page →
Test what you've learned. Watch the lesson and try the interactive quiz on the same page.