Chapters

Hide chapters

Swift Apprentice

1 min

Section III: Building Your Own Types

Section 3: 8 chapters
Show chapters Hide chapters

Section IV: Advanced Topics

Section 4: 13 chapters
Show chapters Hide chapters

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 it 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 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:

func IntInt printMultipleOf (multiplier:, and value:) External name Internal name

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, and 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 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.

You need the return for functions with multiple statements because you might make the function return in many different places.

Advanced parameter handling

Function parameters are constants, 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 and introduce bugs into your code.

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, and it’s a chicken and egg situation. It’s unknown what type value is, so Swift doesn’t know which getValue() to call or the return type of getValue().

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 lose type inference, which is not recommended.

Mini-exercises

  1. Write a function named printFullName that takes two strings called firstName and lastName. The function should print out the full name defined as firstName + " " + lastName. Use it to print out your own full name.
  2. Change the declaration of printFullName to have no external name for either parameter.
  3. Write a function named calculateFullName that returns the full name as a string. Use it to store your own full name in a constant.
  4. 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 the count 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 variable’s name is function . The compiler infers the type 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 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:

  1. function is of a function type that takes two Int parameters and returns an Int, declared like so: (Int, Int) -> Int.
  2. a is of type Int.
  3. b is of type Int.

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 known never to return.

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 that calls the function doesn’t need to bother doing anything after the function call because it knows that it 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. :] ocumenting 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 straightforward way to document functions that 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 Chapter 4, “Advanced Control Flow”, 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 writing 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 ///.
Have a technical question? Want to report a bug? You can ask questions and report bugs to the book authors in our official book forum here.
© 2023 Kodeco Inc.