Intermediate

Functions in Swift

Learn how to define and call functions in Swift, including argument labels, default and variadic parameters, in-out parameters, recursion, and first-class closures with Docker-ready examples

Functions are the primary building blocks of reusable logic in Swift. A function packages a piece of behavior behind a name, accepts typed inputs, and produces a typed output. Swift’s static, strong, and inferred type system means the compiler verifies that every call site passes the right argument types and uses the return value correctly.

What sets Swift functions apart is their expressiveness. Functions support argument labels that make call sites read like prose, default parameter values that reduce overloads, variadic parameters for flexible arity, and in-out parameters for the rare cases where you need to mutate a caller’s variable. Just as importantly, Swift is a multi-paradigm language: functions are first-class values. You can store them in variables, pass them as arguments, and return them from other functions — the foundation of Swift’s functional style.

In this tutorial you’ll learn how to define and call functions, control how parameters are labeled and defaulted, return multiple values with tuples, write recursive functions, and treat functions as values using closures and higher-order functions like map, filter, and reduce.

Defining and Calling Functions

A function is declared with the func keyword, a name, a parenthesized parameter list, and an optional return type introduced by the -> arrow. Inside the body, return produces the result. Functions with no return type simply omit the arrow.

Create a file named functions_basics.swift:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
// A function with no parameters and no return value
func sayHello() {
    print("Hello from a function!")
}

// A function with parameters and a return value
func add(a: Int, b: Int) -> Int {
    return a + b
}

// A function returning a String built with interpolation
func greet(name: String) -> String {
    return "Welcome, \(name)!"
}

sayHello()

let sum = add(a: 3, b: 4)
print("3 + 4 = \(sum)")

print(greet(name: "Swift"))

By default, Swift uses each parameter’s name as its argument label at the call site, which is why you write add(a: 3, b: 4). This self-documenting style is idiomatic Swift.

Argument Labels, Defaults, Variadics, and In-Out

Swift gives you fine-grained control over parameters. You can specify a separate external argument label (or suppress it with _), provide default values, accept a variable number of arguments with a variadic parameter (...), and allow a function to modify a caller’s variable using inout.

Create a file named parameters.swift:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
// External label "to" with internal name "person"; first label suppressed with _
func greet(_ greeting: String, to person: String) -> String {
    return "\(greeting), \(person)!"
}

// Default parameter values let callers omit arguments
func makeCoffee(size: String = "medium", shots: Int = 1) -> String {
    return "A \(size) coffee with \(shots) shot(s)"
}

// A variadic parameter accepts zero or more values as an array
func total(of numbers: Int...) -> Int {
    var sum = 0
    for number in numbers {
        sum += number
    }
    return sum
}

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

print(greet("Hello", to: "Ada"))
print(makeCoffee())
print(makeCoffee(size: "large", shots: 2))
print("Total: \(total(of: 1, 2, 3, 4, 5))")

var score = 21
double(&score)   // pass with & to mark it as in-out
print("Doubled score: \(score)")

Note the & before score: Swift requires you to be explicit at the call site when a function can mutate one of your variables.

Recursion, Tuple Returns, and Scope

Functions can call themselves (recursion) and can return several values at once by returning a tuple. Swift also has clear scoping rules: a variable declared inside a function is local to it, while variables declared at the top level are global.

Create a file named recursion_scope.swift:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
// Recursion: factorial calls itself until it reaches the base case
func factorial(_ n: Int) -> Int {
    if n <= 1 {
        return 1
    }
    return n * factorial(n - 1)
}

// Return multiple values using a named tuple
func minMax(in numbers: [Int]) -> (min: Int, max: Int) {
    var currentMin = numbers[0]
    var currentMax = numbers[0]
    for number in numbers {
        if number < currentMin { currentMin = number }
        if number > currentMax { currentMax = number }
    }
    return (currentMin, currentMax)
}

// Scope: globalCount lives at the top level; message is local
var globalCount = 0
func increment(by step: Int) {
    globalCount += step              // modifies the global
    let message = "Added \(step)"    // local, not visible outside
    print(message)
}

print("5! = \(factorial(5))")

let bounds = minMax(in: [8, 3, 11, 6, 1])
print("Min: \(bounds.min), Max: \(bounds.max)")

increment(by: 10)
increment(by: 5)
print("Global count: \(globalCount)")

Returning a named tuple lets callers access results by meaningful names (bounds.min, bounds.max) instead of positional indices.

Functions as Values: Closures and Higher-Order Functions

Because Swift is also a functional language, functions are first-class values. A function can be passed as an argument, stored in a constant, or returned from another function. Anonymous functions are called closures, written with the { ... in ... } syntax, and they can capture values from their surrounding scope. Swift’s collection types provide higher-order functions like map, filter, and reduce that take closures.

Create a file named higher_order.swift:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
// A function that takes another function as a parameter
func applyTwice(_ value: Int, using transform: (Int) -> Int) -> Int {
    return transform(transform(value))
}

// A named function used as an argument
func square(_ x: Int) -> Int {
    return x * x
}

print("applyTwice square 3 = \(applyTwice(3, using: square))")

// A closure stored in a constant
let addOne = { (x: Int) -> Int in x + 1 }
print("addOne(10) = \(addOne(10))")

// Higher-order functions on collections with trailing-closure syntax
let numbers = [1, 2, 3, 4, 5, 6]
let doubled = numbers.map { $0 * 2 }
let evens = numbers.filter { $0 % 2 == 0 }
let sum = numbers.reduce(0, +)

print("Doubled: \(doubled)")
print("Evens: \(evens)")
print("Sum: \(sum)")

// A function that returns a function, capturing `amount`
func makeAdder(_ amount: Int) -> (Int) -> Int {
    return { value in value + amount }
}

let addFive = makeAdder(5)
print("addFive(20) = \(addFive(20))")

The $0 shorthand refers to the first (and here only) closure argument, letting you write concise transformations like numbers.map { $0 * 2 }.

Running with Docker

You can compile and run every example with the official Swift image — no local toolchain required.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
# Pull the official image
docker pull swift:6.0

# Run the basics example
docker run --rm -v $(pwd):/app -w /app swift:6.0 swift functions_basics.swift

# Run the parameters example
docker run --rm -v $(pwd):/app -w /app swift:6.0 swift parameters.swift

# Run the recursion and scope example
docker run --rm -v $(pwd):/app -w /app swift:6.0 swift recursion_scope.swift

# Run the higher-order functions example
docker run --rm -v $(pwd):/app -w /app swift:6.0 swift higher_order.swift

Expected Output

Running functions_basics.swift:

Hello from a function!
3 + 4 = 7
Welcome, Swift!

Running parameters.swift:

Hello, Ada!
A medium coffee with 1 shot(s)
A large coffee with 2 shot(s)
Total: 15
Doubled score: 42

Running recursion_scope.swift:

5! = 120
Min: 1, Max: 11
Added 10
Added 5
Global count: 15

Running higher_order.swift:

applyTwice square 3 = 81
addOne(10) = 11
Doubled: [2, 4, 6, 8, 10, 12]
Evens: [2, 4, 6]
Sum: 21
addFive(20) = 25

Key Concepts

  • func declares a function with typed parameters and an optional -> return type; the compiler enforces types at every call site.
  • Argument labels make calls readable — Swift uses the parameter name as the label by default, lets you supply a separate external label, or suppress the label with _.
  • Default and variadic parameters reduce the need for overloads: omit defaulted arguments, and pass any number of values to a Type... parameter.
  • In-out parameters (inout) let a function mutate a caller’s variable, but you must opt in explicitly with & at the call site.
  • Tuples return multiple values at once, and naming the tuple elements lets callers read results by name instead of position.
  • Recursion is fully supported — a function may call itself, as long as it has a base case to terminate.
  • Functions are first-class values — store them in constants, pass them as arguments, and return them; closures ({ ... in ... }) are anonymous functions that capture surrounding state.
  • Higher-order functions like map, filter, and reduce express transformations declaratively, reflecting Swift’s functional paradigm alongside its object-oriented and protocol-oriented styles.

Running Today

All examples can be run using Docker:

docker pull swift:6.0
Last updated:

Comments

Loading comments...

Leave a Comment

2000 characters remaining