Functional Programming
|
This section documents the Swift 6 language mode as shipped by Swift 6.3, as published in The Swift Programming Language at docs.swift.org, which is the reference these pages are written and verified against. 6.4-beta-only features are always flagged as such — never presented as baseline. This content was generated with the assistance of AI and should be verified against docs.swift.org before being relied on in production. This section’s bibliography lists the reference material consulted while preparing these pages. |
Swift is not a purely functional language, but its value types, first-class functions, and rich set of higher-order algorithms make a functional style — immutable data, pure functions, composition over mutation — a natural fit alongside its object-oriented and protocol-oriented features.
Immutability and Pure Functions
struct Point { let x, y: Double } // an immutable value type: every mutation produces a new Point
func translated(_ point: Point, dx: Double, dy: Double) -> Point {
Point(x: point.x + dx, y: point.y + dy) // pure: same inputs always produce the same output, no side effects
}
let origin = Point(x: 0, y: 0)
let moved = translated(origin, dx: 3, dy: 4) // origin is untouched -- moved is a new value
Declaring with let and favoring value types (structs, enums — see
Structures and Classes) over classes makes
"changing" a value mean producing a new one rather than mutating shared state, which is what makes a function
like translated(_:dx:dy:) pure: it reads only its arguments, has no observable side effect, and always
returns the same result for the same inputs — callers can reason about it, test it, and run it concurrently
without synchronization, unlike a method that mutates a shared class instance.
First-Class and Higher-Order Functions
func square(_ x: Int) -> Int { x * x }
let operation: (Int) -> Int = square // a function used as an ordinary value
func apply(_ f: (Int) -> Int, to values: [Int]) -> [Int] {
values.map(f) // apply is itself higher-order: it takes a function as a parameter
}
apply(square, to: [1, 2, 3]) // [1, 4, 9]
func adder(_ amount: Int) -> (Int) -> Int { // returns a function -- also makes apply's caller higher-order
{ value in value + amount }
}
let addFive = adder(5)
addFive(10) // 15
A function that can be stored in a variable, passed as an argument, or returned from another function is
first-class — true of every Swift function and closure. A higher-order function either takes a function as
a parameter (apply(:to:)) or returns one (adder(:)); see
Closures for the closure-expression syntax ({ value in … })
used to write one inline.
Composition, Currying and Partial Application
infix operator >>>: AdditionPrecedence
func >>> <A, B, C>(_ f: @escaping (A) -> B, _ g: @escaping (B) -> C) -> (A) -> C {
{ a in g(f(a)) } // custom composition operator: f then g
}
let double = { (x: Int) in x * 2 }
let increment = { (x: Int) in x + 1 }
let doubleThenIncrement = double >>> increment
doubleThenIncrement(5) // 11 -- (5 * 2) + 1
func curriedAdd(_ a: Int) -> (Int) -> Int { // a curried function: takes its arguments one at a time
{ b in a + b }
}
let addTen = curriedAdd(10) // partial application: fixing the first argument
addTen(32) // 42
Function composition builds a new function by feeding one function’s output into another’s input; the custom
>>> operator above is one common convention for writing that left-to-right, building on the operator-overloading
tools in Advanced Operators. Currying is writing a
multi-argument function as a chain of single-argument functions (curriedAdd(_:) returns a function rather than
taking two parameters directly), which makes partial application — fixing some arguments now and supplying
the rest later, as addTen does — fall out naturally, without any dedicated language feature for it.
Recursion
func factorial(_ n: Int) -> Int {
n <= 1 ? 1 : n * factorial(n - 1) // no built-in tail-call optimization guarantee -- deep recursion
} // can still overflow the call stack for large n
indirect enum Expr {
case value(Int)
case add(Expr, Expr)
}
func evaluate(_ expr: Expr) -> Int {
switch expr {
case .value(let v): v
case .add(let a, let b): evaluate(a) + evaluate(b) // recursive descent over a recursive (indirect) enum
}
}
Recursion — a function calling itself, directly or through an indirect enum’s recursive case (see
Enumerations) — is the functional-style alternative to a
loop with mutable state, and reads naturally over tree-shaped data. Swift makes no guarantee of tail-call
optimization, so an iterative loop remains the right choice when recursion depth could be large or unbounded.
map/filter/reduce Pipelines and lazy
let numbers = Array(1...1_000_000)
let result = numbers
.lazy // defers every step below to run element-by-element, on demand
.filter { $0.isMultiple(of: 3) }
.map { $0 * $0 }
.prefix(5) // only the first 5 matches are ever actually computed
Array(result) // [9, 36, 81, 144, 225]
let total = numbers.reduce(0, +) // sums the whole (non-lazy) array eagerly
Chaining map/filter/reduce (covered per-collection in
Collections) expresses a data transformation as a pipeline of
independent steps rather than a hand-written loop with an accumulator. Each step in an eager chain allocates a
full intermediate array, though — .lazy switches the same chain to a LazySequence/LazyCollection that
defers every operation until an element is actually demanded, which matters when only a prefix of a large or
infinite sequence is ever consumed, as .prefix(5) does above.
Optional and Result as Containers
func parseInt(_ text: String) -> Int? { Int(text) }
func reciprocal(_ n: Int) -> Double? { n == 0 ? nil : 1.0 / Double(n) }
let value = parseInt("8")
.map { $0 * 2 } // Optional.map: transform the value if present, else stay nil
.flatMap(reciprocal) // flatMap: chain another Optional-returning step, no double-wrapping
func fetchUser(id: Int) -> Result<String, Error> {
id > 0 ? .success("user\(id)") : .failure(URLError(.badURL))
}
let greeting = fetchUser(id: 7)
.map { "Hello, \($0)" } // Result.map: transform .success, pass .failure through untouched
.flatMap { name in
name.count > 3 ? .success(name) : .failure(URLError(.badURL))
}
Optional and Result (see Optionals and
Error Handling) are both, from a functional standpoint,
containers that may or may not hold a value — Optional unconditionally, Result alongside a specific
failure reason. Their map transforms a contained value without unwrapping it by hand; their flatMap chains
another container-returning step without producing a nested Optional<Optional<T>> or
Result<Result<…>, …>, the same "flatten as you go" shape Sequence.flatMap provides for collections. Both
types compose naturally with protocols and actors — an async function returning Result, or a Sendable
value type flowing through an actor boundary — rather than being at odds with Swift’s other paradigms; see
Protocols and
Actors, Isolation and Sendable.
See Also
-
Closures — closure-expression syntax, capture semantics, and
@escaping/@Sendable, the mechanics behind every higher-order function above. -
Collections — the full
Sequence/Collectionalgorithm set (map,filter,reduce,compactMap, and more) these pipelines draw from. -
Optionals —
Optionalin full, including??and optional chaining. -
Error Handling —
Resultin full, including converting betweenResultandthrows.