Chapter 5. A Modern Paradigm – Closures and Functional Programming

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.

Some programming languages are designed to use only functional programming, but Swift is designed primarily as an object-oriented language with the ability to use functional programming concepts. In this chapter, we will explore how to implement these functional programming concepts in Swift and what they are used for. To do this, we will cover the following topics:

  • Functional programming philosophy
  • Closures
  • Building blocks of functional programming in Swift
  • Lazy evaluation
  • Example

Functional programming philosophy

Before we jump into writing code, let's discuss the ideas and motivations behind functional programming.

State and side effects

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.

In a more general sense, state is any stored information, however temporary, that can be changed. Let's consider a simple double function:

func double(input: Int) -> Int {
    return input * 2
}

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:

struct Ball {
    var radius: Double
    
    mutating func growByAmount(amount: Double) -> Double {
        self.radius = self.radius + amount
        return self.radius
    }
}

If you call this method repeatedly, with the same input on the same Ball instance, you will get a different result every time. This is because there is an additional input in this method, which is the instance it is being called on. It is otherwise referred to as self. self is actually both an input and an output of this method, because the original value of radius affects the output and radius is changed by the end of the method. This is still not very difficult to reason about, as long as you keep in mind that self is always another input and output. However, you can imagine that with a more complex data structure, it can be hard to track every possible input and output from a piece of code. As soon as that starts to happen, it becomes easier for bugs to get created, because we will almost certainly have, unexpected inputs causing unexpected outputs.

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:

mutating func currentRadiusPlusAmount(amount: Double) -> Double {
    self.radius = self.radius + amount
    return self.radius
}

Based on its name, you wouldn't expect this method to change the actual value of radius. This means that if you didn't see the actual implementation, you would expect this method to keep returning the same value if called with the same amount on the same instance. Unpredictability is a terrible thing to have as a programmer.

In its strictest form, functional programming eliminates all state and therefore side effects. We will never go that far in Swift, but we will often use functional programming techniques to reduce state and side effects to increase the predictability of our code, drastically.

Declarative versus imperative code

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:

var originalArray = [1,2,3,4,5]
var greaterThanThree = [Int]()
for num in originalArray {
    if num > 3 {
        greaterThanThree.append(num)
    }
}
print(greaterThanThree) // [4,5]

Running a filter on the array would look similar to this:

var originalArray = [1,2,3,4,5]
var greaterThanThree = originalArray.filter {$0 > 3}
print(greaterThanThree) // [4,5]

Don't worry if you don't understand the second example yet. This is what we are going to cover in the rest of this chapter. The general idea is that with imperative codes, we are going to issue a series of commands with the intent of the code as a secondary, subtler idea. To understand that we are creating a copy of originalArray with only elements greater than 3, we have to read the code and mentally step through what is happening. In the second example, we are stating in the code itself that we are filtering the original array. Ultimately, these ideas exist on a spectrum and it is hard to have something be 100% declarative or imperative, but the principles of each are important.

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.

You can also think of imperative codes as if it were as a factory where each person makes a car in its entirety while thinking of declarative code as if it were a factory with an assembly line. In order to understand what the person is doing in a non-assembly line factory, you have to watch the whole process unfold one step at a time. They will probably be pulling in all kinds of tools at different times and it will be hard to follow. In a factory with an assembly line, you can determine what is happening by looking at each step in the assembly line one at a time.

Now that we understand some of the motivations of functional programming, let's look at the Swift features that make it possible.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset