So far, we have been programming using the paradigm called object-oriented programming, where everything in a program is represented as an object that can be manipulated and passed around to other objects. This is the most popular way to create apps because it is a very intuitive way to think about software and it goes well with the way Apple has designed their frameworks. However, there are some drawbacks to this technique. The biggest one is that the state of data can be very hard to track and reason about. If we have a thousand different objects floating around in our app, all with different information, it can be hard to track down where the bugs occurred and it can be hard to understand how the whole system fits together. Another paradigm of programming that can help with this problem is called functional programming.
Before we jump into writing code, let's discuss the ideas and motivations behind functional programming.
Functional programming makes it significantly easier to think of each component in isolation. This includes things such as types, functions, and methods. If we can wrap our minds around everything that is input into these code components and everything that should be returned from them, we could analyze the code easily to ensure that there are no bugs and it performs well. Every type is created with a certain number of parameters and each method and function in a program has a certain number of parameters and return values. Normally, we think about these as the only inputs and outputs, but the reality is that often there are more. We refer to these extra inputs and outputs as state.
This is a great example of a
stateless function. No matter what else is happening in the entire universe of the program, this method will always return the same value, if it is given the same input. An input of 2
will always return 4
.
Now, let's look at a method with state:
Side effects are an even worse type of extra input or output. They are the unexpected changes to state, seemingly unrelated to the code being run. If we simply rename our preceding method to something a little less clear, its effect on the instance becomes unexpected:
Besides predictability, the other effect that functional programming has on our code is that it becomes more declarative. This means that the code shows us how we expect information to flow through our application. This is in contrast to what we have been doing with object-oriented programming, which we call imperative code. This is the difference between writing a code that loops through an array to add only certain elements to a new array and running a filter on the array. The former would look similar to this:
Running a filter on the array would look similar to this:
So far, with our imperative code, most of it just defines what our data should look like and how it can be manipulated. Even with high quality abstractions, understanding a section of code can often involve jumping between lots of methods, tracing the execution. In declarative code, logic can be more centralized and often more easily read, based on well-named methods.
In Swift, functions are considered first-class citizens, which means that they can be treated the same as any other type. They can be assigned to variables and be passed in and out of other functions. When treated this way, we call them closures. This is an extremely critical piece to write more declarative code because it allows us to treat functionalities like objects. Instead of thinking of functions as a collection of code to be executed, we can start to think about them more like a recipe to get something done. Just like you can give just about any recipe to a chef to cook, you can create types and methods that take a closure to perform some customizable behavior.
Let's take a look at how closures work in Swift. The simplest way to capture a closure in a variable is to define the function and then use its name to assign it to a variable:
As you can see, doubleClosure
can be used just like the normal function name after being assigned. There is actually no difference between using double
and doubleClosure
. Note that we can now think of this closure as an object that will double anything passed to it.
Using this syntax, we can also define our closure inline, such as:
We can define a function to take a closure as a parameter, using the same type syntax we saw previously:
Here, we have a function that can find the first number in an array that passes some arbitrary test. The syntax at the end of the function declaration may be confusing but it should be clear if you work from the inside out. The type for passingTest
is (number: Int) -> Bool
. That is then the second parameter of the whole firstInNumbers
function, which returns an Int?
. If we want to use this function to find the first number greater than three, we can create a custom test and pass that into the function:
We can even define our test right in a call to the function:
First, we can make use of type inference for the type of number
. The compiler knows that number needs to be Int
based on the definition of firstInNumbers:passingTest:
. It also knows that the closure has to return Bool
. Now, we can rewrite our call, as shown:
This looks cleaner, but the parentheses around number
are not required; we could leave those out. In addition, if we have closure as the last parameter of a function, we can provide the closure outside the parentheses for the function call:
This makes it clear that the closure is a test to see which number we want to pull out of the list.
As you can see, doubleClosure
can be used just like the normal function name after being assigned. There is actually no difference between using double
and doubleClosure
. Note that we can now think of this closure as an object that will double anything passed to it.
Using this syntax, we can also define our closure inline, such as:
We can define a function to take a closure as a parameter, using the same type syntax we saw previously:
Here, we have a function that can find the first number in an array that passes some arbitrary test. The syntax at the end of the function declaration may be confusing but it should be clear if you work from the inside out. The type for passingTest
is (number: Int) -> Bool
. That is then the second parameter of the whole firstInNumbers
function, which returns an Int?
. If we want to use this function to find the first number greater than three, we can create a custom test and pass that into the function:
We can even define our test right in a call to the function:
First, we can make use of type inference for the type of number
. The compiler knows that number needs to be Int
based on the definition of firstInNumbers:passingTest:
. It also knows that the closure has to return Bool
. Now, we can rewrite our call, as shown:
This looks cleaner, but the parentheses around number
are not required; we could leave those out. In addition, if we have closure as the last parameter of a function, we can provide the closure outside the parentheses for the function call:
This makes it clear that the closure is a test to see which number we want to pull out of the list.
define a function to take a closure as a parameter, using the same type syntax we saw previously:
Here, we have a function that can find the first number in an array that passes some arbitrary test. The syntax at the end of the function declaration may be confusing but it should be clear if you work from the inside out. The type for passingTest
is (number: Int) -> Bool
. That is then the second parameter of the whole firstInNumbers
function, which returns an Int?
. If we want to use this function to find the first number greater than three, we can create a custom test and pass that into the function:
We can even define our test right in a call to the function:
First, we can make use of type inference for the type of number
. The compiler knows that number needs to be Int
based on the definition of firstInNumbers:passingTest:
. It also knows that the closure has to return Bool
. Now, we can rewrite our call, as shown:
This looks cleaner, but the parentheses around number
are not required; we could leave those out. In addition, if we have closure as the last parameter of a function, we can provide the closure outside the parentheses for the function call:
This makes it clear that the closure is a test to see which number we want to pull out of the list.
This looks cleaner, but the parentheses around number
are not required; we could leave those out. In addition, if we have closure as the last parameter of a function, we can provide the closure outside the parentheses for the function call:
This makes it clear that the closure is a test to see which number we want to pull out of the list.
The first thing to realize is that Swift is not a functional programming language. At its core, it will always be an object-oriented programming language. However, since functions in Swift are first-class citizens, we can use some of the core techniques. Swift provides some built-in methods to get us started.
The first method we are going to discuss is called filter. As the name suggests, this method is used to filter elements in a list. For example, we can filter our numbers
array to include only even numbers:
Swift also provides a method called reduce. The purpose of reduce is to condense a list down to a single value. Reduce works by iterating over every value and combining it with a single value that represents all previous elements. This is just like mixing a bunch of ingredients in a bowl for a recipe. We will take one ingredient at a time and combine it in the bowl until we are left with just a single bowl of ingredients.
Reduce is another great vocabulary item to add to our skill-set. It can reduce any list of information into a single value by analyzing data to generate a document from a list of images and much more.
Map is a method to transform every element in a list to another value. For example, we can add one to every number in the list:
The map method is a great choice to perform calculations on each element of a list, but it should be used only when it makes sense to put the result of the calculation back into a list. You could technically use it to iterate through a list and perform some other action, but in that case, a for-in loop is more appropriate.
The last built-in functional method we will discuss is called sorted. As the name suggests, sorted allows you to change the order of a list. For example, if we want to reorder our numbers list to go from largest to smallest:
There are more built-in functional methods and we will learn to write our own in the next chapter on generics, but these are a core few to help you start thinking about certain problems in a functional way. So how do these methods help us avoid state?
You can also see that complex transformations can all be declared in a concise and centralized place. A reader of the code doesn't need to trace the changing values of many variables; they can simply look at the code and see what processes it will go through.
Swift also provides a method called reduce. The purpose of reduce is to condense a list down to a single value. Reduce works by iterating over every value and combining it with a single value that represents all previous elements. This is just like mixing a bunch of ingredients in a bowl for a recipe. We will take one ingredient at a time and combine it in the bowl until we are left with just a single bowl of ingredients.
Reduce is another great vocabulary item to add to our skill-set. It can reduce any list of information into a single value by analyzing data to generate a document from a list of images and much more.
Map is a method to transform every element in a list to another value. For example, we can add one to every number in the list:
The map method is a great choice to perform calculations on each element of a list, but it should be used only when it makes sense to put the result of the calculation back into a list. You could technically use it to iterate through a list and perform some other action, but in that case, a for-in loop is more appropriate.
The last built-in functional method we will discuss is called sorted. As the name suggests, sorted allows you to change the order of a list. For example, if we want to reorder our numbers list to go from largest to smallest:
There are more built-in functional methods and we will learn to write our own in the next chapter on generics, but these are a core few to help you start thinking about certain problems in a functional way. So how do these methods help us avoid state?
You can also see that complex transformations can all be declared in a concise and centralized place. A reader of the code doesn't need to trace the changing values of many variables; they can simply look at the code and see what processes it will go through.
Reduce is another great vocabulary item to add to our skill-set. It can reduce any list of information into a single value by analyzing data to generate a document from a list of images and much more.
Map is a method to transform every element in a list to another value. For example, we can add one to every number in the list:
The map method is a great choice to perform calculations on each element of a list, but it should be used only when it makes sense to put the result of the calculation back into a list. You could technically use it to iterate through a list and perform some other action, but in that case, a for-in loop is more appropriate.
The last built-in functional method we will discuss is called sorted. As the name suggests, sorted allows you to change the order of a list. For example, if we want to reorder our numbers list to go from largest to smallest:
There are more built-in functional methods and we will learn to write our own in the next chapter on generics, but these are a core few to help you start thinking about certain problems in a functional way. So how do these methods help us avoid state?
You can also see that complex transformations can all be declared in a concise and centralized place. A reader of the code doesn't need to trace the changing values of many variables; they can simply look at the code and see what processes it will go through.
The map method is a great choice to perform calculations on each element of a list, but it should be used only when it makes sense to put the result of the calculation back into a list. You could technically use it to iterate through a list and perform some other action, but in that case, a for-in loop is more appropriate.
The last built-in functional method we will discuss is called sorted. As the name suggests, sorted allows you to change the order of a list. For example, if we want to reorder our numbers list to go from largest to smallest:
There are more built-in functional methods and we will learn to write our own in the next chapter on generics, but these are a core few to help you start thinking about certain problems in a functional way. So how do these methods help us avoid state?
You can also see that complex transformations can all be declared in a concise and centralized place. A reader of the code doesn't need to trace the changing values of many variables; they can simply look at the code and see what processes it will go through.
There are more built-in functional methods and we will learn to write our own in the next chapter on generics, but these are a core few to help you start thinking about certain problems in a functional way. So how do these methods help us avoid state?
You can also see that complex transformations can all be declared in a concise and centralized place. A reader of the code doesn't need to trace the changing values of many variables; they can simply look at the code and see what processes it will go through.
A powerful feature of Swift is the ability to make these operations lazily evaluated. This means that, just like a lazy person would do, a value is only calculated when it is absolutely necessary and at the latest point possible.
This even applies to looping through a result:
Each number is converted to a string only upon the next iteration of the for-in loop. If we were to break out of that loop early, the rest of the values would not be calculated. This is a great way to save processing time, especially on large lists.
Let's take a look at what this looks like in practice. We can use some of the techniques we learned in this chapter to write a different and possibly better implementation of our party inviter.
We can start by defining the same input data:
So let's write the random genre
function:
Now we can use that function to map the invitees to a list of invitations:
Here we try to pick a random genre. If we can't, we return an invitation saying that the invitee should just bring themselves. If we can, we return an invitation saying what genre they should bring with the example show. The one new thing to note here is that we are using the sequence "\n"
in our string. This is a newline character and it signals that a new line should be started in the text.
Now, all we have to do is add a call to this function to our sequence:
This implementation is not necessarily better than our previous implementations, but it definitely has its advantages. We have taken steps towards reducing the state by implementing it as a series of data transformations. The big hiccup in that is that we are still maintaining state in the genre dictionary. We can certainly do more to eliminate that as well, but this gives you a good idea of how we can start to think about problems in a functional way. The more ways in which we can think about a problem, the higher our odds of coming up with the best solution.