5.
Functions
Written by Matt Galloway
Functions are a core part of many programming languages. Simply put, a function lets you define a block of code that performs a task. Then, whenever your app needs to execute that task, you can run the function instead of copying and pasting the same code everywhere.
In this chapter, you’ll learn how to write your own functions, and see firsthand how Swift makes them easy to use.
Function basics
Imagine you have an app that frequently needs to print your name. You can write a function to do this:
func printMyName() {
print("My name is Matt Galloway.")
}
The code above is known as a function declaration. You define a function using the func
keyword. After that comes the name of the function, followed by parentheses. You’ll learn more about the need for these parentheses in the next section.
After the parentheses comes an opening brace, followed by the code you want to run in the function, followed by a closing brace. With your function defined, you can use it like so:
printMyName()
This prints out the following:
My name is Matt Galloway
.
If you suspect that you’ve already used a function in previous chapters, you’re correct! print
, which prints the text you give it to the console, is indeed a function. This leads nicely into the next section, in which you’ll learn how to pass data to a function and get data back in return.
Function parameters
In the previous example, the function simply prints out a message. That’s great, but sometimes you want to parameterize your function, which lets the function perform differently depending on the data passed into it via its parameters.
As an example, consider the following function:
func printMultipleOfFive(value: Int) {
print("\(value) * 5 = \(value * 5)")
}
printMultipleOfFive(value: 10)
Here, you can see the definition of one parameter inside the parentheses after the function name, named value
and of type Int
. In any function, the parentheses contain what’s known as the parameter list. These parentheses are required both when declaring and when invoking the function, even if the parameter list is empty. This function will print out any given multiple of five. In the example, you call the function with an argument of 10, so the function prints the following:
10 * 5 = 50
Note: Take care not to confuse the terms “parameter” and “argument”. A function declares its parameters in its parameter list. When you call a function, you provide values as arguments for the functions’ parameters.
You can take this one step further and make the function more general. With two parameters, the function can print out a multiple of any two values.
func printMultipleOf(multiplier: Int, andValue: Int) {
print("\(multiplier) * \(andValue) = \(multiplier * andValue)")
}
printMultipleOf(multiplier: 4, andValue: 2)
There are now two parameters inside the parentheses after the function name: one named multiplier
and the other named andValue
, both of type Int
.
Notice that you need to apply the labels in the parameter list to the arguments when you call a function. In the example above, you need to put multiplier:
before the multiplier and andValue:
before the value to be multiplied.
In Swift, you should try to make your function calls read like a sentence. In the example above, you would read the last line of code like this:
Print multiple of multiplier 4 and value 2
You can make this even clearer by giving a parameter a different external name. For example, you can change the name of the andValue
parameter:
func printMultipleOf(multiplier: Int, and value: Int) {
print("\(multiplier) * \(value) = \(multiplier * value)")
}
printMultipleOf(multiplier: 4, and: 2)
You assign a different external name by writing it in front of the parameter name. In this example, the internal name of the parameter is now value
while the external name (the argument label) in the function call is now and
. You can read the new call as:
Print multiple of multiplier 4 and 2
The following diagram explains where the external and internal names come from in the function declaration:
The idea behind this is to allow you to have a function call be readable in a sentence like manner, but still have an expressive name within the function itself. You could have written the above function like so:
func printMultipleOf(multiplier: Int, and: Int)
This would have the same effect at the function call of being a nice readable sentence. However now the parameter inside the function is also called and
. In a long function, it could get confusing to have such a generically named parameter.
If you want to have no external name at all, then you can employ the underscore _
, as you’ve seen in previous chapters:
func printMultipleOf(_ multiplier: Int, and value: Int) {
print("\(multiplier) * \(value) = \(multiplier * value)")
}
printMultipleOf(4, and: 2)
This change makes it even more readable at the call-site. The function call now reads like so:
Print multiple of 4 and 2
You could, if you so wished, take this even further and use _
for all parameters, like so:
func printMultipleOf(_ multiplier: Int, _ value: Int) {
print("\(multiplier) * \(value) = \(multiplier * value)")
}
printMultipleOf(4, 2)
In this example, all parameters have no external name. But this illustrates how you use the underscore wisely. Here, your expression is still understandable, but more complex functions that take many parameters can become confusing and unwieldy with no external parameter names. Imagine if a function took five parameters!
You can also give default values to parameters:
func printMultipleOf(_ multiplier: Int, _ value: Int = 1) {
print("\(multiplier) * \(value) = \(multiplier * value)")
}
printMultipleOf(4)
The difference is the = 1
after the second parameter, which means that if no value is provided for the second parameter, it defaults to 1
.
Therefore, this code prints the following:
4 * 1 = 4
It can be useful to have a default value when you expect a parameter to be one particular value most of the time, and it will simplify your code when you call the function.
Return values
All of the functions you’ve seen so far have performed a simple task: printing something out. Functions can also return a value. The caller of the function can assign the return value to a variable or constant, or use it directly in an expression.
With a return value, you can use a function to transform data. You simply take in data through parameters, perform computations and return the result.
Here’s how you define a function that returns a value:
func multiply(_ number: Int, by multiplier: Int) -> Int {
return number * multiplier
}
let result = multiply(4, by: 2)
To declare that a function returns a value, you add a ->
followed by the type of the return value after the set of parentheses and before the opening brace. In this example, the function returns an Int
.
Inside the function, you use a return
statement to return the value. In this example, you return the product of the two parameters.
It’s also possible to return multiple values through the use of tuples:
func multiplyAndDivide(_ number: Int, by factor: Int)
-> (product: Int, quotient: Int) {
return (number * factor, number / factor)
}
let results = multiplyAndDivide(4, by: 2)
let product = results.product
let quotient = results.quotient
This function returns both the product and quotient of the two parameters: It returns a tuple containing two Int
values with appropriate member value names.
The ability to return multiple values through tuples is one of the many things that makes it such a pleasure to work with Swift. And it turns out to be a handy feature, as you’ll see shortly.
You can make both of these functions simpler by removing the return
, like so:
func multiply(_ number: Int, by multiplier: Int) -> Int {
number * multiplier
}
func multiplyAndDivide(_ number: Int, by factor: Int)
-> (product: Int, quotient: Int) {
(number * factor, number / factor)
}
You can do this because the function is a single statement. If the function had more lines of code in it, then you wouldn’t be able to do this. The idea behind this feature is that in such simple functions it’s so obvious and the return
gets in the way of readability. For longer functions you need the return
because you might make the function return in many different places.
Advanced parameter handling
Function parameters are constants by default, which means they can’t be modified.
To illustrate this point, consider the following code:
func incrementAndPrint(_ value: Int) {
value += 1
print(value)
}
This results in an error:
Left side of mutating operator isn't mutable: 'value' is a 'let' constant
The parameter value
is the equivalent of a constant declared with let
. Therefore, when the function attempts to increment it, the compiler emits an error.
It is important to note that Swift copies the value before passing it to the function, a behavior known as pass-by-value.
Note: Pass-by-value and making copies is the standard behavior for all of the types you’ve seen so far in this book. You’ll see another way for things to be passed into functions in Chapter 13, “Classes”.
Usually, you want this behavior. Ideally, a function doesn’t alter its parameters. If it did, you couldn’t be sure of the parameters’ values and you might make incorrect assumptions in your code, leading to the erroneous data.
Sometimes you do want to let a function change a parameter directly, a behavior known as copy-in copy-out or call by value result. You do it like so:
func incrementAndPrint(_ value: inout Int) {
value += 1
print(value)
}
inout
before the parameter type indicates that this parameter should be copied in, that local copy used within the function, and copied back out when the function returns.
You need to make a slight tweak to the function call to complete this example. Add an ampersand (&
) before the argument, which makes it clear at the call site that you are using copy-in copy-out:
var value = 5
incrementAndPrint(&value)
print(value)
Now the function can change the value however it wishes.
This example will print the following:
6
6
The function increments value
and keeps its modified data after the function finishes. The value goes in to the function and comes back out again, thus the keyword inout
.
Under certain conditions, the compiler can simplify copy-in copy-out to what is called pass-by-reference. The argument value isn’t copied into the parameter. Instead, the parameter will hold a reference to the memory of the original value. This optimization satisfies all requirements of copy-in copy-out while removing the need for copies.
Overloading
Did you notice how you used the same function name for several different functions in the previous examples?
func printMultipleOf(multiplier: Int, andValue: Int)
func printMultipleOf(multiplier: Int, and value: Int)
func printMultipleOf(_ multiplier: Int, and value: Int)
func printMultipleOf(_ multiplier: Int, _ value: Int)
This is called overloading and lets you define similar functions using a single name.
However, the compiler must still be able to tell the difference between these functions. Whenever you call a function, it should always be clear which function you’re calling.
This is usually achieved through a difference in the parameter list:
- A different number of parameters.
- Different parameter types.
- Different external parameter names, such as the case with
printMultipleOf
.
You can also overload a function name based on a different return type, like so:
func getValue() -> Int {
31
}
func getValue() -> String {
"Matt Galloway"
}
Here, there are two functions called getValue()
, which return different types–one an Int
and the other a String
.
Using these is a little more complicated. Consider the following:
let value = getValue()
How does Swift know which getValue()
to call? The answer is, it doesn’t. And it will print the following error:
error: ambiguous use of 'getValue()'
There’s no way of knowing which one to call. It’s a chicken and egg situation. It’s unknown what type value
is, so Swift doesn’t know which getValue()
to call or what the return type of getValue()
should be.
To fix this, you can declare what type you want value
to be, like so:
let valueInt: Int = getValue()
let valueString: String = getValue()
This will correctly call the Int
version of getValue()
in the first instance, and the String
version of getValue()
in the second instance.
It’s worth noting that overloading should be used with care. Only use overloading for functions that are related and similar in behavior.
When only the return type is overloaded, as in the above example, you loose type inference and so is not recommended.
Mini-exercises
- Write a function named
printFullName
that takes two strings calledfirstName
andlastName
. The function should print out the full name defined asfirstName + " " + lastName
. Use it to print out your own full name. - Change the declaration of
printFullName
to have no external name for either parameter. - Write a function named
calculateFullName
that returns the full name as a string. Use it to store your own full name in a constant. - Change
calculateFullName
to return a tuple containing both the full name and the length of the name. You can find a string’s length by using thecount
property. Use this function to determine the length of your own full name.
Functions as variables
This may come as a surprise, but functions in Swift are simply another data type. You can assign them to variables and constants just as you can any other type of value, such as an Int
or a String
.
To see how this works, consider the following function:
func add(_ a: Int, _ b: Int) -> Int {
a + b
}
This function takes two parameters and returns the sum of their values.
You can assign this function to a variable, like so:
var function = add
Here, the name of the variable is function
and its type is inferred as (Int, Int) -> Int
from the add
function you assign to it.
Notice how the function type (Int, Int) -> Int
is written in the same way you write the parameter list and return type in a function declaration.
Here, the function
variable is of a function type that takes two Int
parameters and returns an Int
.
Now you can use the function
variable in just the same way you’d use add
, like so:
function(4, 2)
This returns 6.
Now consider the following code:
func subtract(_ a: Int, _ b: Int) -> Int {
a - b
}
Here, you declare another function that takes two Int
parameters and returns an Int
. You can set the function
variable from before to your new subtract
function, because the parameter list and return type of subtract
is compatible with the type of the function
variable.
function = subtract
function(4, 2)
This time, the call to function
returns 2.
The fact that you can assign functions to variables comes in handy because it means you can pass functions to other functions. Here’s an example of this in action:
func printResult(_ function: (Int, Int) -> Int, _ a: Int, _ b: Int) {
let result = function(a, b)
print(result)
}
printResult(add, 4, 2)
printResult
takes three parameters:
-
function
is of a function type that takes twoInt
parameters and returns anInt
, declared like so:(Int, Int) -> Int
. -
a
is of typeInt
. -
b
is of typeInt
.
printResult
calls the passed-in function, passing into it the two Int
parameters. Then it prints the result to the console:
6
It’s extremely useful to be able to pass functions to other functions, and it can help you write reusable code. Not only can you pass data around to manipulate, but passing functions as parameters also means you can be flexible about what code executes.
The land of no return
Some functions are never, ever, intended to return control to the caller. For example, think about a function that is designed to crash an application. Perhaps this sounds strange, so let me explain: if an application is about to work with corrupt data, it’s often best to crash rather than continue into an unknown and potentially dangerous state. The function fatalError("reason to terminate")
is an example of a function like this. It prints the reason for the fatal error and then halts execution to prevent further damage.
Another example of a non-returning function is one that handles an event loop. An event loop is at the heart of every modern application that takes input from the user and displays things on a screen. The event loop services requests coming from the user, then passes these events to the application code, which in turn causes the information to be displayed on the screen. The loop then cycles back and services the next event.
These event loops are often started in an application by calling a function that is known to never return. Once you’re coding iOS or macOS apps, think back to this paragraph when you encounter UIApplicationMain
or NSApplicationMain
.
Swift will complain to the compiler that a function is known to never return, like so:
func noReturn() -> Never {
}
Notice the special return type Never
, indicating that this function will never return.
If you wrote this code you would get the following error:
Function with uninhabited return type 'Never' is missing call to another never-returning function on all paths
This is a rather long-winded way of saying that the function doesn’t call another “no return” function before it returns itself. When it reaches the end, the function returns to the place from which it was called, breaching the contract of the Never
return type.
A crude, but honest, implementation of a function that wouldn’t return would be as follows:
func infiniteLoop() -> Never {
while true {
}
}
You may be wondering why bother with this special return type. It’s useful because by the compiler knowing that the function won’t ever return, it can make certain optimizations when generating the code to call the function. Essentially, the code which calls the function doesn’t need to bother doing anything after the function call, because it knows that this function will never end before the application is terminated.
Writing good functions
Functions let you solve many problems. The best do one simple task , making them easier to mix, match, and model into more complex behaviors.
Make functions that are easy to use and understand! Give them well-defined inputs that produce the same output every time. You’ll find it’s easier to reason about and test good, clean, simple functions in isolation.
Commenting your functions
All good software developers document their code. :]
Documenting your functions is an important step to making sure that when you return to the code later or share it with other people, it can be understood without having to trawl through the code.
Fortunately Swift has a very easy way to document functions which integrates well with Xcode’s code completion and other features.
It uses the defacto Doxygen commenting standard used by many other languages outside of Swift. Let’s take a look at how you can document a function:
/// Calculates the average of three values
/// - Parameters:
/// - a: The first value.
/// - b: The second value.
/// - c: The third value.
/// - Returns: The average of the three values.
func calculateAverage(of a: Double, and b: Double, and c: Double) -> Double {
let total = a + b + c
let average = total / 3
return average
}
calculateAverage(of: 1, and: 3, and: 5)
Instead of the usual double-/
, you use triple-/
instead. Then the first line is the description of what the function does. Following that is a list of the parameters and finally, a description of the return value.
If you forget the format of a documentation comment, simply highlight the function and press “Option-Command-/” in Xcode. The Xcode editor will insert a comment template for you that you can then fill out.
When you create this kind of code documentation, you will find that the comment changes the font in Xcode from the usual monospace font. Neat right? Well, yes, but there’s more.
First, Xcode shows your documentation when code completion comes up, like so:
Also, you can hold the option key and click on the function name, and Xcode shows your documentation in a handy popover, like so:
Both of these are very useful and you should consider documenting all your functions, especially those that are frequently used or complicated. Future you will thank you later. :]
Challenges
Before moving on, here are some challenges to test your knowledge of functions. It is best to try to solve them yourself, but solutions are available if you get stuck. These came with the download or are available at the printed book’s source code link listed in the introduction.
Challenge 1: Looping with stride functions
In the last chapter you wrote some for
loops with countable ranges. Countable ranges are limited in that they must always be increasing by one. The Swift stride(from:to:by:)
and stride(from:through:by:)
functions let you loop much more flexibly.
For example, if you wanted to loop from 10 to 20 by 4’s you can write:
for index in stride(from: 10, to: 22, by: 4) {
print(index)
}
// prints 10, 14, 18
for index in stride(from: 10, through: 22, by: 4) {
print(index)
}
// prints 10, 14, 18, and 22
- What is the difference between the two stride function overloads?
- Write a loop that goes from 10.0 to (and including) 9.0, decrementing by 0.1.
Challenge 2: It’s prime time
When I’m acquainting myself with a programming language, one of the first things I do is write a function to determine whether or not a number is prime. That’s your second challenge.
First, write the following function:
func isNumberDivisible(_ number: Int, by divisor: Int) -> Bool
You’ll use this to determine if one number is divisible by another. It should return true
when number
is divisible by divisor
.
Hint: You can use the modulo (%
) operator to help you out here.
Next, write the main function:
func isPrime(_ number: Int) -> Bool
This should return true
if number
is prime, and false
otherwise. A number is prime if it’s only divisible by 1 and itself. You should loop through the numbers from 1 to the number and find the number’s divisors. If it has any divisors other than 1 and itself, then the number isn’t prime. You’ll need to use the isNumberDivisible(_:by:)
function you wrote earlier.
Use this function to check the following cases:
isPrime(6) // false
isPrime(13) // true
isPrime(8893) // true
Hint 1: Numbers less than 0 should not be considered prime. Check for this case at the start of the function and return early if the number is less than 0.
Hint 2: Use a for
loop to find divisors. If you start at two and end before the number itself, then as soon as you find a divisor, you can return false
.
Hint 3: If you want to get really clever, you can simply loop from 2 until you reach the square root of number
, rather than going all the way up to number
itself. I’ll leave it as an exercise for you to figure out why. It may help to think of the number 16, whose square root is 4. The divisors of 16 are 1, 2, 4, 8 and 16.
Challenge 3: Recursive functions
In this challenge, you will see what happens when a function calls itself, a behavior called recursion. This may sound unusual, but it can be quite useful.
You’re going to write a function that computes a value from the Fibonacci sequence. Any value in the sequence is the sum of the previous two values. The sequence is defined such that the first two values equal 1. That is, fibonacci(1) = 1
and fibonacci(2) = 1
.
Write your function using the following declaration:
func fibonacci(_ number: Int) -> Int
Then, verify you’ve written the function correctly by executing it with the following numbers:
fibonacci(1) // = 1
fibonacci(2) // = 1
fibonacci(3) // = 2
fibonacci(4) // = 3
fibonacci(5) // = 5
fibonacci(10) // = 55
Hint 1: For values of number
less than 0, you should return 0.
Hint 2: To start the sequence, hard-code a return value of 1 when number
equals 1 or 2.
Hint 3: For any other value, you’ll need to return the sum of calling fibonacci
with number - 1
and number - 2
.
Key points
- You use a function to define a task that you can execute as many times as you like without having to write the code multiple times.
- Functions can take zero or more parameters and optionally return a value.
- You can add an external name to a function parameter to change the label you use in a function call, or you can use an underscore to denote no label.
- Parameters are passed as constants, unless you mark them as
inout
, in which case they are copied-in and copied-out. - Functions can have the same name with different parameters. This is called overloading.
- Functions can have a special
Never
return type to inform Swift that this function will never exit. - You can assign functions to variables and pass them to other functions.
- Strive to create functions that are clearly named and have one job with repeatable inputs and outputs.
- Function documentation can be created by prefixing the function with a comment section using
///
.