Swift From Scratch: Function Parameters, Types, and Nesting

In the previous article, we explored the basics of functions in Swift. Functions, however, have a lot more to offer. In this article, we continue our exploration of functions and look into function parameters, nesting, and types.

1. Local and External Parameter Names

Parameter Names

Let us revisit one of the examples from the previous article. The printMessage(message:) function defines one parameter, message.

We assign a name, message, to the parameter and use this name when we call the function.

But notice that we also use the same name to reference the value of the parameter in the body of the function.

In Swift, a parameter always has a local parameter name, and it optionally has an external parameter name. In the example, the local and external parameter names are identical.

API Guidelines

As of Swift 3, the Swift team has defined a clear set of API guidelines. I won't go into those guidelines in this tutorial, but I want to point out that the definition of the printMessage(message:) function deviates from those guidelines. The name of the function contains the word message, and the parameter is also named message. In other words, we are repeating ourselves.

It would be more elegant if we could invoke the printMessage(message:) function without the message keyword. This is what I have in mind.

This is possible, and it is more in line with the Swift API guidelines. But what is different? The difference is easy to spot if we take a look at the updated function definition. The updated example also reveals more about the anatomy of functions in Swift.

In a function definition, each parameter is defined by an external parameter name, a local parameter name, a colon, and the type of the parameter. If the local and external parameter names are identical, we only write the parameter name once. That is why the first example defines one parameter name, message.

If we don't want to assign an external parameter name to a parameter, we use the _, an underscore. This informs the compiler that the parameter doesn't have an external parameter name, and that means we can omit the parameter name when the function is invoked.

External Parameter Names

Objective-C is known for its long method names. While this may look clunky and inelegant to outsiders, it makes methods easy to understand and, if chosen well, very descriptive. The Swift team understood this advantage and introduced external parameter names from day one.

When a function accepts several parameters, it isn't always obvious which argument corresponds to which parameter. Take a look at the following example to better understand the problem. Notice that the parameters don't have an external parameter name.

The power(_:_:) function raises the value of a by the exponent b. Both parameters are of type Int. While most people will intuitively pass the base value as the first argument and the exponent as the second argument, this isn't clear from the function's type, name, or signature. As we saw in the previous article, invoking the function is straightforward.

To avoid confusion, we can give the parameters of a function external names. We can then use these external names when the function is called to unambiguously indicate which argument corresponds to which parameter. Take a look at the updated example below.

Note that the function's body hasn't changed since the local names haven't changed. However, when we invoke the updated function, the difference is clear and the result is less confusing.

While the types of both functions are identical, (Int, Int) -> Int, the functions are different. In other words, the second function isn't a redeclaration of the first function. The syntax to invoke the second function may remind you of Objective-C. Not only are the arguments clearly described, but the combination of function and parameter names also describe the purpose of the function.

In some cases, you want to use the same name for the local and the external parameter name. This is possible, and there's no need to type the parameter name twice. In the following example, we use base and exponent as the local and external parameter names.

By defining one name for each parameter, the parameter name serves as the local and external name of the parameter. This also means that we need to update the body of the function.

It's important to note that by providing an external name for a parameter, you are required to use that name when invoking the function. This brings us to default values.

Default Values

We covered default parameter values in the previous article. This is the function we defined in that article.

What happens if we don't define an external parameter name for the second parameter, which has a default value?

The compiler doesn't seem to care. But is this what we want? It is best to define an external parameter name to optional parameters (parameters with a default value) to avoid confusion and ambiguity.

Notice that we are repeating ourselves again in the previous example. There is no need to define an external parameter name for the date parameter. The next example shows what the printDate(_:format:) function would look like if we followed the Swift API guidelines.

We can now invoke the formatDate(_:format:) function without using the date label for the first parameter and with an optional date format.

2. Parameters and Mutability

Let us revisit the first example of this tutorial, the printMessage(_:) function. What happens if we change the value of the message parameter inside the function's body?

It doesn't take long for the compiler to start complaining.

Parameters and Mutability

The parameters of a function are constants. In other words, while we can access the values of function parameters, we cannot change their value. To work around this limitation, we declare a variable in the function's body and use that variable instead.

3. Variadic Parameters

While the term may sound odd at first, variadic parameters are common in programming. A variadic parameter is a parameter that accepts zero or more values. The values need to be of the same type. Using variadic parameters in Swift is trivial, as the following example illustrates.

The syntax is easy to understand. To mark a parameter as variadic, you append three dots to the parameter's type. In the function body, the variadic parameter is accessible as an array. In the above example, args is an array of Int values.

Because Swift needs to know which arguments correspond to which parameters, a variadic parameter is required to be the last parameter. It also implies that a function can have at most one variadic parameter.

The above also applies if a function has parameters with default values. The variadic parameter should always be the last parameter.

4. In-Out Parameters

Earlier in this tutorial, you learned that the parameters of a function are constants. If you want to pass a value into a function, modify it in the function, and pass it back out of the function, in-out parameters are what you need.

The following example shows an example of how in-out parameters work in Swift and what the syntax looks like.

We define the first parameter as an in-out parameter by adding the inout keyword. The second parameter is a regular parameter with an external name of withString and a local name of prefix. How do we invoke this function?

We declare a variable, input, of type String and pass it to the prefixString(_:with:) function. The second parameter is a string literal. By invoking the function, the value of the input variable becomes Hello, world!. Note that the first argument is prefixed with an ampersand, &, to indicate that it is an in-out parameter.

It goes without saying that constants and literals cannot be passed in as in-out parameters. The compiler throws an error when you do as illustrated in the following examples.

In-Out Parameters

It's evident that in-out parameters cannot have default values or be variadic. If you forget these details, the compiler kindly reminds you with an error.

5. Nesting

In C and Objective-C, functions and methods cannot be nested. In Swift, however, nested functions are quite common. The functions we saw in this and the previous article are examples of global functions—they are defined in the global scope.

When we define a function inside a global function, we refer to that function as a nested function. A nested function has access to the values defined in its enclosing function. Take a look at the following example to better understand this.

While the functions in this example aren't terribly useful, they illustrate the idea of nested functions and capturing values. The printHelloWorld() function is only accessible from within the printMessage(_:) function.

As illustrated in the example, the printHelloWorld() function has access to the constant a. The value is captured by the nested function and is therefore accessible from within that function. Swift takes care of capturing values, including managing the memory of those values.

6. Function Types

Functions as Parameters

In the previous article, we briefly touched upon function types. A function has a particular type, composed of the function's parameter types and its return type. The printMessage(_:) function, for example, is of type (String) -> (). Remember that () symbolizes Void, which is equivalent to an empty tuple.

Because every function has a type, it's possible to define a function that accepts another function as a parameter. The following example shows how this works.

The printMessage(_:with:) function accepts a string as its first parameter and a function of type (String) -> () as its second parameter. In the function's body, the function that we pass in is invoked with the message argument.

The example also illustrates how we can invoke the printMessage(_:with:) function. The myMessage constant is passed in as the first argument and the printMessage(_:) function as the second argument. How cool is that?

Functions as Return Types

It is also possible to return a function from a function. The next example is a bit contrived, but it illustrates what the syntax looks like.

The compute(_:) function accepts a boolean and returns a function of type (Int, Int) -> Int. The compute(_:) function contains two nested functions that are also of type (Int, Int) -> Int, add(_:_:) and subtract(_:_:).

The compute(_:) function returns a reference to either the add(_:_:) or the subtract(_:_:) function, based on the value of the addition parameter.

The example also shows how to use the compute(_:) function. We store a reference to the function that is returned by the compute(_:) function in the computeFunction constant. We then invoke the function stored in computeFunction, passing in 1 and 2, store the result in the result constant, and print the value of result in the standard output. The example may look complex, but it is actually easy to understand if you know what is going on.

Conclusion

You should now have a good understanding of how functions work in Swift and what you can do with them. Functions are fundamental to the Swift language, and you will use them extensively when working with Swift.

In the next article, we dive head first into closures—a powerful construct reminiscent of blocks in C and Objective-C, closures in JavaScript, and lambdas in Ruby.

If you want to learn how to use Swift 3 to code real-world apps, check out our course Create iOS Apps With Swift 3. Whether you're new to iOS app development or are looking to make the switch from Objective-C, this course will get you started with Swift for app development. 

 


Tags:

Comments

Related Articles