DEV Community

Cover image for Understanding Swift Closures - Part 1
Tamas Fodor
Tamas Fodor

Posted on

Understanding Swift Closures - Part 1

“Any fool can write code that a computer can understand. Good programmers write code that humans can understand.”
― Martin Fowler

As I'm a committed participant of the 100DaysOfSwift challenge by Paul Hudson, it was inevitable to meet closures at some point. Even though I have a solid understanding of what closures (or lambdas) are from other languages and why they're useful, I realized that closures are one of the main reasons why I'm struggling with reading others' Swift code.

After day 7, I didn't want to just simply let it go so I decided to dig deeper. The reason why I'm writing about it is that I'd like to collect my findings and solidify the knowledge I've got during the journey. Hopefully, you will find it beneficial and take the value that is relevant to you.

Alright, let's get started.

What are closures?

According to the official documentation, "Closures are self-contained blocks of functionality that can be passed around and used in your code". The same applies to functions so I'm not far if I'm saying they're functions basically but you can write and use them in a few different ways compared to normal functions in the hope of better code quality.

First-class objects

In programming, the term "First-class" means that you can assign these so-called first-class objects to variables, pass them as function parameters and return them as the result of function execution. This is not different from functions in Swift.
Before you think you have to use the closure syntax in order to assign a function to a variable or pass it as a function parameter, I have to point it out that it's just not true. It's completely up to you which one you choose, normal functions or closures. In my opinion, one is not better than the other and it depends on what works the best for you. If it's easier for you to understand your code by using normal functions then go for it. But it's needless to say that having a decent knowledge around closures is very important, especially if you're not the only developer who's working on a project or you're about to go through a hiring process at your dream company.

The function type annotation

"So I can juggle with functions as I do with Strings, Integers or other types. So far so good, but why would I do that?" Before jumping into the practical parts, let's see how you would write normal functions and closures in Swift.

Since Swift is a strongly typed language, you have to add type annotations at some certain degree to your variables. Fortunately, Swift is smart enough to figure the type of a variable by simply the value you've assigned to it. This is the so-called Type Inference. But let's just be super explicit for now and optimize it later.

In the following example, there's a normal function called countLetters which accepts a String and returns an Integer.

func countLetters(word: String) -> Int {
    return word.count
}

let stillCountLetters: (String) -> Int = countLetters
Enter fullscreen mode Exit fullscreen mode

The function countLetters is assigned to a new variable stillCountLetters. As we agreed, let's be explicit and set the type of stillCountLetters which is (String) -> Int. It means "Hey, I can store a pointer to a function that accepts a String (String) and returns an Integer -> Int". If the arity of a function is zero, which means it doesn't accept anything, you can write an opening and a closing parenthesis and nothing between the two (). If your function returns nothing, you can use the Void type after ->.

So in short, when a function accepts nothing and returns nothing, the type of this function looks this:

let myFunc: () -> Void
Enter fullscreen mode Exit fullscreen mode

At the place of the function declaration, you can omit the type of the return value only if your function returns nothing. That's going to be an implicit Void:

func printMessage(message: String) {
    print(message)
}
Enter fullscreen mode Exit fullscreen mode

instead of Void, you can also use parentheses but for me, that's just too many of them and at the first glance, it confuses me with Tuples which doesn't make sense of course but still:

let myFunc: () -> ()

func myFunc2() -> () {
}
Enter fullscreen mode Exit fullscreen mode

The closure syntax

Alright, in the previous section, we created a normal function and assigned it to a variable. It's time to see how we can write closures.

let countLetters: (String) -> Int = { (word: String) -> Int in
    return word.count
}
Enter fullscreen mode Exit fullscreen mode

As you can see, the type is the same as it is for normal functions. Everything inside the curly brackets is the functionality belonging to the closure. Before the in keyword, you can find the incoming values (word: String) and the type of the return value -> Int. Everything after in is what your function does. What you would normally put between the brackets in case of a normal function.

Why closures?

At this point, you might be like "Ok, this is the closure syntax, fine! It's a little bit different from a normal function. Why do you want to confuse me, Apple?". Yeah, I hear you. It's like the "tabs versus spaces" war, isn't it?
Well, it's not that simple and there are obvious reasons why Apple has introduced closures in Swift.

The main reason behind the idea of closures is convenience. It provides a convenient way to work with functions. It's kind of like following the "Write less, do more" principle. At the beginning when your codebase consists of just a few lines of code it doesn't seem to make any difference. But later on when you're about to scale your application, you add some new features, bugs start to show up and you eventually end up maintaining a huge monster that is not so easy to debug anymore. Not to mention if you have to work with other developers on the project or even worse, you've inherited a big legacy codebase.

When it comes to creating and maintaining a scalable and testable application which is easy to reason about, it's very common lately to follow the Functional Programming Paradigm in order to write code that meets the aforementioned requirements. Long story short, if FP is your thing and you heavily rely on functions, Swift provides a kind of like a more user-friendly way to deal with them. Period.

In the following sections, I'll go through the cases when closures might come in handy in the future in general or if you decide to get on the Functional Programming bandwagon.

Omitting labels

One of my favorite things is that it's not required to use labels when you invoke a closure with parameters. Moreover, it doesn't even allow you to use them at all. The following code will fail at compile time:

let countLetters = { (word: String) -> Int in 
    return word.count
}

countLetters(word: "Raptors") // error: extraneous argument label 'word:' in call ...
Enter fullscreen mode Exit fullscreen mode

You cannot override the labels for external usage either:

let countLetters = { (of word: String) -> Int in 
    return word.count
}

countLetters(of: "Raptors") // error: closure cannot have keyword arguments
Enter fullscreen mode Exit fullscreen mode

Don't get me wrong. I see and understand the benefits of labels and after reading a few articles by Robert Martin, I'd probably feel like I want to use them everywhere. But I'm not yet used to labels and I always tend to forget writing them when I call functions and they're not optional.

Write less, do more

This section describes a few features available in Swift that help you write shorter code for the sake of readability but it can be dangerous too if you use them inappropriately.

Creating closures in place

As I said earlier, functions are first-class citizens which means you can pass them as function parameters among others. And it's affordable with both normal functions and closures. Suppose you have a function to add two numbers and another function called combine that takes a function with which you can combine that two numbers in a certain way. For now, don't try to understand what the point is of passing functions to other functions. I'll describe it later.

func add(a: Int, b: Int) -> Int {
    return a + b
}

func combine(a: Int, b: Int, by function: (Int, Int) -> Int) -> Int {
    return function(a, b)
}

let result = combine(a: 2, b: 3, by: add)

print(result) // 5
Enter fullscreen mode Exit fullscreen mode

It's totally fine to do this. We're extremely explicit here but it's still valid Swift.

With closures, you can shorten the code above a bit:

func combine(a: Int, b: Int, by function: (Int, Int) -> Int) -> Int {
    return function(a, b);
}

let result = combine(a: 2, b: 3, by: { (a: Int, b: Int) -> Int in 
    return a + b
})

print(result) // 5
Enter fullscreen mode Exit fullscreen mode

You cannot create normal functions at the place of the invocation. By using closures in our example, we could save 3 lines of code. It's not much but imagine a bigger module with hundreds of lines of code. By specifying how our combine function should do the math, we improve readability so the developer doesn't have to jump to another place in the codebase to see what add does when she's investigating our code. By doing this we can avoid "Change blindness":

Change blindness is a perceptual phenomenon that occurs when a change in a visual stimulus is introduced and the observer does not notice it. For example, observers often fail to notice major differences introduced into an image while it flickers off and on again. People's poor ability to detect changes has been argued to reflect fundamental limitations of human attention.

from Wikipedia

And we can keep staying in the flow state:

In positive psychology, a flow state, also known colloquially as being in the zone, is the mental state of operation in which a person performing an activity is fully immersed in a feeling of energized focus, full involvement, and enjoyment in the process of the activity.

from Wikipedia

The power of Type Inference

As I referred to it previously, If you write your code consistently, Swift is smart enough to figure the types automagically due to Type Inference. Having this in mind, we can do further enhancements.

Since it's stated already in the combine function that the return type of the function parameter must be an Integer, we can omit the type of the return value of the closure when we pass it:

let result = combine(a: 2, b: 3, by: { (a: Int, b: Int) in 
    return a + b
})
Enter fullscreen mode Exit fullscreen mode

The same applies to arguments:

let result = combine(a: 2, b: 3, by: { (a, b) in 
    return a + b
})
Enter fullscreen mode Exit fullscreen mode

If you really want, the code is short enough to write it in one line. But personally, I prefer spreading vertically rather than horizontally:

let result = combine(a: 2, b: 3, by: { (a, b) in return a + b })
Enter fullscreen mode Exit fullscreen mode

Implicit returns

However we could reduce the size of our code to 1 line and 64 characters long (The recommended maximum length is 80 before putting the next line), it's still not the best in terms of readability. We can go further by taking advantage of implicit returns inside closures. If the body of your closure contains only one statement and you want to return the result of it, you can omit the return keyword.

let result = combine(a: 2, b: 3, by: { (a, b) in a + b })
Enter fullscreen mode Exit fullscreen mode

Shorthand arguments

Swift does even let you omit the arguments and use the shorthand version of them. The shorthand syntax is pointing to an item by using its index in the argument list starting from zero. You must prefix the index with a dollar sign. So in our case, the shorthand version of a and b is $0 and $1. By using shorthand arguments you can also omit the in keyword:

let result = combine(a: 2, b: 3, by: { $0 + $1 })
Enter fullscreen mode Exit fullscreen mode

Alright, I think the time has come to point it out that however, it's a very fancy way of solving the problem, it really hurts readability. If we're just experimenting or just hacking something, it's fine. To be fair, even if I'm a less experienced Swift developer, I can presume that this closure adds two numbers. But don't forget that we can concatenate two strings together with the + operator. It's not necessarily clear at first glance what the types are of the two parameters. If XCode is your preferred editor to write Swift, tooltips can come in handy to figure it out. When it comes to making a decision whether you should use shorthand arguments, especially if it's a very complex functionality, think twice, think of your co-workers and your future self.

Operator methods

Still not enough?! Ah, you're killing me! Alright! If there's only one statement inside your closure and it's a basic arithmetic operation or comparing a value to another, it's enough to only write the operator and Swift does the rest for you:

let result = combine(a: 2, b: 3, by: +)
Enter fullscreen mode Exit fullscreen mode

BONUS:

Did you know that you don't even have to write anything at all??? Swift can read your mind, so the following code does work like a charm:

let result = combine(a: 2, b: 3, by:)
Enter fullscreen mode Exit fullscreen mode

Just kidding. It doesn't. Sorry for my awkward sense of humor. :(

Trailing closures

Hurray, we've finally arrived at the practical parts. So why would you use closures? Well, that's a very good question but better to ask yourself why would you use Functional Programming. Even if you're not even aware of that, probably you do FP pretty often if you're a full-time Swift developer.

Suppose we have the following view. When the view appears on the screen, it shows a popup window with a greeting. Let's take a closer look inside the viewDidAppear function where the popup window is created and told to be displayed. It creates an alert view and an action, adds the action to the alert and then opens the popup.

import UIKit

class ViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()
    }

    func alertActionHandler(_ action: UIAlertAction) {
        print("Great!")
    }

    func alertCompletionHandler() {
        print("Completion feels good.");
    }

    override func viewDidAppear(_ animated: Bool) {

        let alert = UIAlertController(title: "Greeting", message: "Hello, World!", preferredStyle: .alert)
        let action = UIAlertAction(title: "OK", style: .default, handler: alertActionHandler)

        alert.addAction(action)

        self.present(alert, animated: true, completion: alertCompletionHandler)
    }
}
Enter fullscreen mode Exit fullscreen mode

The third argument of the UIAlertAction class accepts a closure called handler. It's like "Alright, action! I'm giving you this function. Would you please so kind, and call it for me when the user hits the action button?".
I'm 100% sure, you've already met these kinds of things and yes, it's Functional Programming. When you pass a function to another function and wait until a certain event happens (User clicks on the OK button in our case), this is the so-called callback design pattern in other languages like NodeJS. It may have a different name in Swift but the concept is the same. It's pretty powerful if you don't want your application to stop until the click event.

As I said earlier, using closures is optional. It's just a convenient way to work with callbacks. If you don't like the idea, you can stay with normal functions. In the example above, I declared the alertActionHandler callback as a member of the ViewController class and passed it to UIAlertAction. Just a quick side note, you can also declare the function inside viewDidAppear method as a nested function. It's pretty powerful if you want to reach variables from the parent context but more on this later.

This is a very good example to demonstrate the power of closures, more precisely the trailing closures. Let's refactor this code and see how it feels like after replacing the class methods with closures. Let's just not use a trailing closure fist.

import UIKit

class ViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()
    }

    func alertCompletionHandler() {
        print("Completion feels good.");
    }

    override func viewDidAppear(_ animated: Bool) {

        let alert = UIAlertController(title: "Greeting", message: "Hello, World!", preferredStyle: .alert)
        let action = UIAlertAction(title: "OK", style: .default, handler: { _ in
            print("Great!")
        })

        alert.addAction(action)

        self.present(alert, animated: true, completion: alertCompletionHandler)
    }
}
Enter fullscreen mode Exit fullscreen mode

In my opinion, this is much better because it doesn't grab the user out from the flow since we've declared the callback in place so she doesn't have to scroll up to the class method. The promise of the trailing closure syntax is easier readability so let's see how it looks like. To help you focus on the relevant parts, I've removed the rest of the code:

    let action = UIAlertAction(title: "OK", style: .default) { _ in
        print("Great!")
    }
Enter fullscreen mode Exit fullscreen mode

Here's the thing. If and only if the last parameter of a given function is a closure, you're allowed to use the trailing closure syntax. As you can see, by using it, you can omit the label.
I let you decide if it's easier to read compared to the previous example. If you prefer being more explicit and using labels, it's completely up to you.

You might have noticed that there was another place in the code where I applied the callback pattern. The class's present method accepts a closure as the last parameter so we can take advantage of the trailing closure syntax again to make the code a bit shorter.

import UIKit

class ViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()
    }

    override func viewDidAppear(_ animated: Bool) {

        let alert = UIAlertController(title: "Greeting", message: "Hello, World!", preferredStyle: .alert)
        let action = UIAlertAction(title: "OK", style: .default) { _ in
            print("Great!")
        }

        alert.addAction(action)

        self.present(alert, animated: true) {
            print("Completion feels good.");
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Capturing values

You can also return a function from other functions. Remember? When you declare a function inside of another, it's a nested function. It can be very powerful if you want to store some state in the parent context (which is the function in which the nested function was created) based on certain inputs. The nested function has access to the variables in the parent context. When you call the function that returns a function, it's like "Ok I did what you wanted me to do, here's a function if you want to go further and do other stuff later (based on the stored state)."

In the following code, there's an addUser function with which you can add a user to the database:

var users = Set<String>()

func addUser(_ user: String) -> () -> Void {

    users.insert(user)

    func removeUser() {
        users.remove(user)
    }

    return removeUser
}

let removeJohn = addUser("John")

print(users) // ["John"]

removeJohn()

print(users) // []
Enter fullscreen mode Exit fullscreen mode

It's a bit silly example but straightforward enough to get the idea. addUser is responsible for adding the given user name to the users Set. It also returns a function with which you can remove the user you just added previously. You don't need to remember how the addUser does its job. It doesn't matter what Collection type it uses behind the scene. If you want to add John, just call the function with the input "John", store the returned remove function which belongs to a parent context where John has been stored as a new user.

We can say that addUser is a nested function inside the global context. So even though I declared the users Set outside from the function, it can "capture" that variable.

Let's see how it looks like with closures:

var users = Set<String>()

func addUser(_ user: String) -> () -> Void {

    users.insert(user)

    return {
        users.remove(user)
    }
}

let removeJohn = addUser("John")

print(users)

removeJohn()

print(users)
Enter fullscreen mode Exit fullscreen mode

Alright, that's all for now. I hope you have a better understanding of closures and find them useful. If not, that's fine too. At the end of the day, what matters is whether or not your App works. Next time I'm going to show you other dirty secrets of closures in Swift. If I missed something important or I was wrong, let me know in the comments section.

Take care!

Top comments (0)