Swift Learning (8) - Functions (Improved Code Version)

Swift Learning (8) - Functions (Improved Code Version)

August 22, 2021·Jingyao Zhang
Jingyao Zhang

image

A function is an independent code segment that completes a specific task. You can identify a function’s purpose by its name, which can be used to “call” the function whenever you need it to perform its task.

Swift’s unified function syntax is very flexible and can represent any function, from the simplest C-style functions without parameter names to complex Objective-C-style functions with local and external parameter names. Parameters can provide default values to simplify function calls. Parameters can also be used as both input and output parameters, meaning that once the function finishes executing, the values of the input parameters may be modified.

In Swift, every function has a type composed of the types of its parameter values and return value. You can treat function types like any other ordinary variable type, making it easier to pass functions as parameters to other functions or return functions from other functions. Function definitions can also be written inside other function definitions, enabling encapsulation of functionality within nested function scopes.


Function Definition and Calling

When defining a function, you can define one or more named and typed values as the function’s input, called parameters, and you can also define a value of a certain type as the output when the function finishes executing, called the return type.

Every function has a function name that describes the task it performs. To use a function, call it by its name and pass it matching input values (called arguments). The arguments must be in the same order as the parameters in the function’s parameter list.

In the example below, the function is named greet(person:). It takes a person’s name as input and returns a greeting for that person. To accomplish this, it defines an input parameter: a String value called person, and a return value of type String containing the greeting for that person.

func greet(person: String) -> String {
        let greeting = "Hello, " + person + "!"
        return greeting
}

All of this information together is called the function’s definition, and it starts with the func keyword. To specify the return type, use a return arrow -> (a hyphen followed by a right angle bracket) followed by the name of the return type.

This definition describes what the function does, what it expects as parameters, and what type of result it returns when finished. Such a definition allows the function to be called elsewhere in a clear way:

print(greet(person: "Jensen"))  // Prints "Hello, Jensen!"
print(greet(person: "Morris"))  // Prints "Hello, Morris!"
---
output: Hello, Jensen!
Hello, Morris!

When calling the greet(person:) function, you pass it a String argument in parentheses, such as greet(person: "Anna"). As shown above, since this function returns a String, greet can be included in a call to print(_:separator:terminator:) to output the function’s return value.

Note

The first parameter of the print(_:separator:terminator:) function does not have a label, and the other parameters are optional because they have default values. For more details about these function syntax variations, see the section below on function parameter labels, parameter names, and default parameter values.

In the body of greet(person:), a new constant named greeting is defined, and the greeting message for personName is assigned to it. Then, the return keyword is used to return the greeting. Once return greeting is called, the function ends its execution and returns the current value of greeting.

To simplify this definition, you can write the creation and return of the greeting message in one line:

func greetAgain(person: String) -> String {
        return "Hello again, " + person + "!"
}
print(greetAgain(person: "Jensen"))  // Prints "Hello again, Jensen!"
---
output: Hello again, Jensen!

Function Parameters and Return Values

Function parameters and return values are very flexible in Swift. You can define any type of function, from simple functions with a single unnamed parameter to complex functions with expressive parameter names and various parameter options.

Functions with No Parameters

A function can have no parameters. The following function is a parameterless function that returns a fixed String message when called:

func sayHelloWorld() -> String {
        return "Hello, World!"
}
print(sayHelloWorld())  // Prints "Hello, World!"
---
output: Hello, World!

Even though this function has no parameters, you still need a pair of parentheses after the function name in its definition and when calling it.

Functions with Multiple Parameters

A function can have multiple input parameters, which are included in the function’s parentheses and separated by commas.

The following function takes a person’s name and a Boolean indicating whether they have already been greeted, and returns an appropriate greeting:

func greet(person: String, alreadyGreeted: Bool) -> String {
        if alreadyGreeted {
                return greetAgain(person: person)
        } else {
                return greet(person: person)
        }
}
print(greet(person: "Jensen", alreadyGreeted: true))  // Prints "Hello again, Jensen!"
---
output: Hello again, Jensen!

You can call the greet(person:alreadyGreeted:) function by passing a String value and a Bool value labeled alreadyGreeted, separated by a comma inside the parentheses. Note that this function is different from the greet(person:) function above. Although they share the same name, greet(person:alreadyGreeted:) requires two parameters, while greet(person:) requires only one.

Functions with No Return Value

A function can have no return value. Here is another version of the greet(person:) function, called greets, which prints a String value directly instead of returning it:

func greets(person: String) {
        print("Hello, \(person)!")
}
greets(person: "Jensen")  // Prints "Hello, Jensen!"
---
output: Hello, Jensen!

Since this function does not need to return a value, there is no return arrow -> or return type in its definition.

Note

Strictly speaking, even if no return value is explicitly defined, the greet(person:) function still returns a value. Functions without an explicit return type return a special value of type Void, which is an empty tuple written as ().

You can ignore the return value when calling a function:

func printAndCount(string: String) -> Int {
        print(string)
        return string.count
}
func printWithoutCounting(string: String) {
        let _ = printAndCount(string: string)
}
printAndCount(string: "Hello, World")  // Prints "Hello, World" and returns 12
printWithoutCounting(string: "Hello, World")  // Prints "Hello, World" but returns nothing
---
output: Hello, World
Hello, World

The first function, printAndCount(string:), prints a string and returns the character count as an Int. The second function, printWithoutCounting(string:), calls the first function but ignores its return value. When the second function is called, the message is still printed by the first function, but the return value is not used.

Note

Return values can be ignored, but a function defined with a return value must return a value. If you do not return a value at the end of the function, it will result in a compile-time error.

Functions with Multiple Return Values

You can use tuple types to return multiple values as a composite value from a function.

The following example defines a function called minMax(array:) that finds the minimum and maximum values in an array of Int values.

func minMax(array: [Int]) -> (min: Int, max: Int) {
        var currentMin = array[0]
        var currentMax = array[0]
        for value in array[1..<array.count] {
                if value < currentMin {
                        currentMin = value
                } else if value > currentMax {
                        currentMax = value
                }
        }
        return (currentMin, currentMax)
}

The minMax(array:) function returns a tuple containing two Int values, labeled min and max, so you can access them by name.

In the body of minMax(array:), two working variables, currentMin and currentMax, are initialized to the first value in the array. The function then iterates over the remaining values in the array, checking if each value is smaller than currentMin or larger than currentMax. Finally, the minimum and maximum values are returned as a tuple.

Since the tuple members are named, you can access the minimum and maximum values using dot syntax:

let bounds = minMax(array: [8, -6, 2, 109, 3, -71])
print("Min is \(bounds.min) and max is \(bounds.max).")  // Prints "Min is -71 and max is 109."
---
output: Min is -71 and max is 109.

Note that you do not need to name the tuple members when returning the tuple from the function, as their names are already specified in the function’s return type.

Optional Tuple Return Types

If a function’s tuple return type might not have a value at all, you can use an optional tuple return type to reflect that the entire tuple can be nil. You define an optional tuple by placing a question mark after the closing parenthesis, such as (Int, Int)? or (String, Int, Bool)?

Note

An optional tuple type like (Int, Int)? is different from a tuple containing optional types like (Int?, Int?). An optional tuple means the entire tuple is optional, not just the individual elements.

The previous minMax(array:) function returns a tuple containing two Int values, but it does not perform any safety checks on the input array. If the array parameter is empty, accessing array[0] will cause a runtime error.

To safely handle the “empty array” case, rewrite the minMax(array:) function to use an optional tuple return type and return nil when the array is empty:

func MinMax(array: [Int]) -> (min: Int, max: Int)? {
        if array.isEmpty {
                return nil
        }
        var currentMin = array[0]
        var currentMax = array[0]
        for value in array[1..<array.count] {
                if value < currentMin {
                        currentMin = value
                } else if value > currentMax {
                        currentMax = value
                }
        }
        return (currentMin, currentMax)
}

You can use optional binding to check whether the minMax(array:) function returns a tuple value or nil:

if let bounds = MinMax(array: [8, -6, 2, 109, 3, -71]) {
        print("Min is \(bounds.min) and max is \(bounds.max).")
}  // Prints "Min is -71 and max is 109."
if let bounds = MinMax(array: []) {
        print("Min is \(bounds.min) and max is \(bounds.max).")
} else {
        print("Array is null.")
}
---
output: Min is -71 and max is 109.
Array is null.

Implicitly Returned Functions

If the entire body of a function is a single expression, the function can implicitly return that expression. For example, the following functions have the same effect:

func greeting(for person: String) -> String {
        "Hello, " + person + "!"
}
print(greeting(for: "Jensen"))  // Prints "Hello, Jensen!"
func anotherGreeting(for person: String) -> String {
        return "Hello, " + person + "!"
}
print(anotherGreeting(for: "Jensen"))  // Prints "Hello, Jensen!"
---
output: Hello, Jensen!
Hello, Jensen!

The complete definition of the greeting(for:) function is the return of the greeting content, which means it can use the more concise implicit return form. The anotherGreeting(for:) function returns the same content but is longer because of the return keyword. Any function that can be written as a single return statement can omit return.

As you will see in the “Shorthand Getter Declaration” section, a property’s getter can also use the implicit return form.


Function Parameter Labels and Parameter Names

Every function parameter has a parameter label (Argument Label) and a parameter name (Parameter Name). The parameter label is used when calling the function; you need to write the parameter label before the corresponding argument. The parameter name is used in the function’s implementation. By default, function parameters use their parameter names as their parameter labels.

func someFunction(firstParameterName: Int, secondParameterName: Int) {
        // Inside the function body, firstParameterName and secondParameterName refer to the first and second parameter values
}
someFunction(firstParameterName: 1, secondParameterName: 2)

All parameters must have a unique name. Although it is possible for multiple parameters to have the same parameter label, a unique parameter label makes the code more readable.

Specifying Parameter Labels

You can specify a parameter label before the parameter name, separated by a space:

func someFunction(argumentLabel parameterName: Int) {
        // Inside the function body, parameterName refers to the parameter value
}

The following version of the greet(person:) function takes a person’s name and their hometown, and returns a greeting:

func greet(person: String, from hometown: String) -> String {
        "Hello \(person)! Glad you could visit from \(hometown)."
}
print(greet(person: "Jensen", from: "Hefei"))  // Prints "Hello Jensen! Glad you could visit from Hefei."
---
output: Hello Jensen! Glad you could visit from Hefei.

Using parameter labels makes a function call more expressive and natural, while still maintaining readability and clear intent inside the function.

Omitting Parameter Labels

If you do not want to add a label for a parameter, you can use an underscore _ to replace an explicit parameter label.

func someFunction(_ firstParameterName: Int, secondParameterName: Int) {
        // Inside the function body, firstParameterName and secondParameterName refer to the first and second parameter values
}
someFunction(1, secondParameterName: 2)

If a parameter has a label, you must use the parameter label when calling the function.

Default Parameter Values

You can define a default value for any parameter by assigning a value to it in the function body. When a default value is defined, you can omit that parameter when calling the function.

func someFunction(parameterWithoutDefault: Int, parameterWithDeafult: Int = 12) {
        // If the second parameter is not passed when calling, parameterWithDefault will be assigned the default value 12 in the function body.
}
someFunction(parameterWithoutDefault: 1)  // parameterWithDefault is 12
someFunction(parameterWithoutDefault: 2, parameterWithDeafult: 6)  // parameterWithDefault is 6

Place parameters without default values at the beginning of the parameter list. Generally, parameters without default values are more important, and placing them first ensures that the order of non-default parameters is consistent when calling the function, making calls to the same function in different situations clearer.

Variadic Parameters

A variadic parameter can accept zero or more values. When calling a function, you can use a variadic parameter to specify that the parameter can accept an indeterminate number of input values. You define a variadic parameter by adding ... after the parameter’s type name.

The values passed to a variadic parameter become an array of that type inside the function body. For example, a variadic parameter called numbers of type Double... becomes a constant array of type [Double] named numbers inside the function body.

The following function calculates the arithmetic mean of a group of numbers of any length:

func arithmeticMean(_ numbers: Double...) -> Double {
        var total: Double = 0
        for number in numbers {
                total += number
        }
        return total / Double(numbers.count)
}
print(arithmeticMean(1, 2, 3, 4, 5))  // Prints 3.0, the mean of these five numbers
print(arithmeticMean(3, 8.25, 18.75))  // Prints 10.0, the mean of these three numbers
---
output: 3.0
10.0

A function can have multiple variadic parameters. The first argument after a variadic parameter must have an argument label. The argument label is used to distinguish whether the argument is passed to the variadic parameter or the following parameter.

In-Out Parameters

Function parameters are constants by default. Attempting to change a parameter’s value inside the function body will result in a compile-time error. This prevents accidental modification of parameter values. If you want a function to be able to modify a parameter’s value and have those changes persist after the function call, you need to define the parameter as an in-out parameter.

To define an in-out parameter, add the inout keyword before the parameter definition. An in-out parameter has a value passed into the function, is modified by the function, and then is passed out of the function, replacing the original value. For more details about in-out parameters and related compiler optimizations, see the section on in-out parameters.

You can only pass variables to in-out parameters, not constants or literals, because those cannot be modified. When passing a parameter as an in-out parameter, you need to prefix the parameter name with &, indicating that the value can be modified by the function.

Note

In-out parameters cannot have default values, and variadic parameters cannot be marked as inout.

In the example below, the swapTwoInts(_:_:) function has two in-out parameters named a and b:

func swapTwoInts(_ a: inout Int, _ b: inout Int) {
        let temporaryA = a
        a = b
        b = temporaryA
}

The swapTwoInts(_:_:) function simply swaps the values of a and b. It first stores the value of a in a temporary constant temporaryA, then assigns the value of b to a, and finally assigns temporaryA to b.

You can call swapTwoInts(_:_:) with two Int variables. Note that both someInt and anotherInt are prefixed with & before being passed to the function.

var someInt = 3
var anotherInt = 107
swapTwoInts(&someInt, &anotherInt)
print("SomeInt is now \(someInt), and anotherInt is now \(anotherInt)!")  // Prints "SomeInt is now 107, and anotherInt is now 3!"
---
output: SomeInt is now 107, and anotherInt is now 3!

As you can see from the example above, the original values of someInt and anotherInt are modified inside the swapTwoInts(_:_:) function, even though they are defined outside the function body.

Note

In-out parameters are different from return values. The swapTwoInts function above does not define any return value, but still modifies the values of someInt and anotherInt. In-out parameters are another way for a function to have an effect outside its body.


Function Types

Every function has a specific function type, which is composed of the function’s parameter types and return type. For example:

func addTwoInts(_ a: Int, _ b: Int) -> Int {
        a + b
}
func multiplyTwoInts(_ a: Int, _ b: Int) -> Int {
        a * b
}

This example defines two simple math functions: addTwoInts and multiplyTwoInts. Both functions take two Int values and return an Int value.

The type of these two functions is (Int, Int) -> Int, which can be interpreted as:

  • “This function type has two parameters of type Int and returns a value of type Int.”

Here is another example, a function with no parameters and no return value:

func printHelloWorld() {
        print("Hello, world!")
}

The type of this function is () -> Void, or “a function with no parameters that returns Void.”

Using Function Types

In Swift, you use function types just like other types. For example, you can define a constant or variable of a function type and assign an appropriate function to it.

var mathFunc: (Int, Int) -> Int = addTwoInts

This code can be interpreted as:

  • “Define a variable called mathFunc of type ‘a function with two Int parameters that returns an Int’, and assign it to the addTwoInts function.”

Since addTwoInts and mathFunc have the same type, this assignment is allowed by Swift’s type checker.

Now you can use mathFunc to call the assigned function:

print("Result: \(mathFunc(2, 3)).")  // Prints "Result: 5."
---
output: Result: 5.

Different functions with matching types can be assigned to the same variable, just like non-function types:

mathFunc = multiplyTwoInts
print("Result: \(mathFunc(2, 3)).")  // Prints "Result: 6."
---
output: Result: 6.

As with other types, when assigning a function to a constant or variable, you can let Swift infer its function type:

let anotherMathFunc = addTwoInts
// anotherMathFunc is inferred to be of type (Int, Int) -> Int

Function Types as Parameter Types

You can use a function type like (Int, Int) -> Int as the parameter type of another function, allowing part of the function’s implementation to be provided by the caller:

func printMathResult(_ mathFunc: (Int, Int) -> Int, _ a: Int, _ b: Int) {
        print("Result: \(multiplyTwoInts(a, b)). ")
}
printMathResult(mathFunc, 8, 2)  // Prints "Result: 16."
---
output: Result: 16. 

This example defines a function printMathResult(_:_:_:) with three parameters: the first parameter, mathFunc, is of type (Int, Int) -> Int and can accept any function of that type; the second and third parameters, a and b, are both Int values to be passed as input to the given function.

When printMathResult(_:_:_:) is called, it is passed the multiplyTwoInts function and the integers 8 and 2. It calls multiplyTwoInts with 8 and 2 and prints the result, 16.

The purpose of the printMathResult(_:_:_:) function is to print the result of calling another math function of the appropriate type. It does not care how the passed-in function is implemented, only that it is of the correct type, allowing printMathResult(_:_:_:) to delegate part of its functionality to the caller in a type-safe way.

Function Types as Return Types

You can use a function type as the return type of another function by writing a complete function type after the return arrow ->.

The following example defines two simple functions, stepForward(_:) and stepBackward(_:). The stepForward(_:) function returns a value one greater than its input, and stepBackward(_:) returns a value one less than its input. Both functions have the type (Int) -> Int:

func stepForward(_ input: Int) -> Int {
        input + 1
}
func stepBackward(_ input: Int) -> Int {
        input - 1
}

The following function, chooseStepFunc(backward:), has a return type of (Int) -> Int. It returns either the stepForward(_:) or stepBackward(_:) function based on the Boolean value backward:

func chooseStepFunc(backward: Bool) -> (Int) -> Int {
        backward ? stepBackward : stepForward
}

Now you can use chooseStepFunc(backward:) to get one of the two functions:

var  currentValue = 3
let moveNearerToZero = chooseStepFunc(backward: currentValue > 0)

In the example above, it is determined whether currentValue should move toward zero by increasing or decreasing. The initial value of currentValue is 3, so currentValue > 0 is true, causing chooseStepFunc(backward:) to return the stepBackward(_:) function. A reference to the returned function is stored in the constant moveNearerToZero.

Now that moveNearerToZero points to the correct function, it can be used to count down to zero:

print("Counting to zero:")
// Counting to zero:
while currentValue != 0 {
        print("\(currentValue)...")
        currentValue = moveNearerToZero(currentValue)
}
print("Zero!")  // 3...  // 2...  // 1...  // Zero!
---
output: Counting to zero:
3...
2...
1...
Zero!

Nested Functions

So far, all the functions you have seen in this chapter are called global functions, defined in the global scope. You can also define functions inside other function bodies, called nested functions.

By default, nested functions are not visible to the outside world, but can be called by their enclosing function. An enclosing function can also return one of its nested functions, making it available in other scopes.

You can rewrite the chooseStepFunc(backward:) function using a nested function (chooseStepFunction()):

func chooseStepFunction(backward: Bool) -> (Int) -> Int {
        func stepForward(input: Int) -> Int { return input + 1 }
        func stepBackward(input: Int) -> Int { return input - 1 }
        return backward ? stepBackward(input:) : stepForward(input:)
}
var CurrentValue = -4
let MoveNearerToZero = chooseStepFunction(backward: currentValue > 0)  // moveNearerToZero now refers to the nested stepForward() function
while CurrentValue != 0 {
        print("\(CurrentValue)...")
        CurrentValue = MoveNearerToZero(CurrentValue)
}
print("Zero!")  // -4...  // -3...  // -2...  // -1...  // Zero!
---
output: -4...
-3...
-2...
-1...
Zero!
Last updated on