How to use @autoclosure in Swift to improve performance

@autoclosure in Swift is a type of closure that allows to omit braces and make it look like a normal expression. Under the hood, however, it’s still a closure. By understanding what this means, we can improve the efficiency of our code.

The @autoclosure keyword might be new to you. For many of us, it’s hard to come up with use-cases for it. However, if you look closely, you’ll notice that it’s used in standard Swift APIs you’re using every day.

What is an @autoclosure?

It’s all in the name: @autoclosure automatically creates a closure from an argument passed to a function. Turning an argument into a closure allows us to delay the actual request of the argument.

Let’s explain this in more detail using the following code example. In this example, we’ve created a debugLog method and a Person struct which we’re going to print out:

 struct Person: CustomStringConvertible {
     let name: String
     
     var description: String {
         print("Asking for Person description.")
         return "Person name is \(name)"
     }
 }

 let isDebuggingEnabled: Bool = false
 
 func debugLog(_ message: String) {
     /// You could replace this in projects with #if DEBUG
     if isDebuggingEnabled {
         print("[DEBUG] \(message)")
     }
 }

 let person = Person(name: "Bernie")
 debugLog(person.description)
 
 // Prints:
 // Asking for Person description. 

Even-though we disabled debugging, the Person structure is still asked for its description. This is because the message argument of debugLog is directly computed.

We can solve this by making use of a closure:

 let isDebuggingEnabled: Bool = false

 func debugLog(_ message: () -> String) {
     /// You could replace this in projects with #if DEBUG
     if isDebuggingEnabled {
         print("[DEBUG] \(message())")
     }
 }

 let person = Person(name: "Bernie")
 debugLog({ person.description })

 // Prints:
 // -

The message() closure call is only called when debugging is enabled. You can see that we now need to pass in a closure argument to the debugLog method which doesn’t look so nice.

We can improve this code by making use of the @autoclosure keyword:

 let isDebuggingEnabled: Bool = false
 
 func debugLog(_ message: @autoclosure () -> String) {
     /// You could replace this in projects with #if DEBUG
     if isDebuggingEnabled {
         print("[DEBUG] \(message())")
     }
 }

 let person = Person(name: "Bernie")
 debugLog(person.description)

 // Prints:
 // - 

The logic within the debugLog method stays the same and still has to work with a closure. However, on the implementation level, we can now pass on the argument as if it were a normal expression. It looks both clean and familiar while we did optimize our debug logging code.

@autoclosure allows delaying an argument’s actual computing, just like we’ve seen before with lazy collections and lazy properties. In fact, if debugging is not enabled, we’re no longer computing debug descriptions while we did before!

Examples of standard Swift APIs using @autoclosure

Now that we know how @autoclosure works, we can briefly look at standard APIs using this keyword.

A common example is the assert(condition:message:file:line:) function. Its condition is only evaluated #if DEBUG is true and its message is only called if the condition failed. Both arguments are auto closures. In fact, many of the testing APIs use auto closures.

Lastly, there’s another blog post I wrote covering a useful example combining an @autoclosure with an autoreleasepool.

Conclusion

An @autoclosure can be a great solution to prevent unnecessary work if code isn’t actually used. On the implementation level, everything looks the same, while under the hood, we optimized our code.

If you like to improve your Swift knowledge, even more, check out the Swift category page. Feel free to contact me or tweet to me on Twitter if you have any additional tips or feedback.

Thanks!