Using @autoclosure to design Swift APIs#
This article is a translation#
Original article link: Using @autoclosure when designing Swift APIs
The Swift @autoclosure attribute is used to define a "wrapped" parameter in a closure. It is mainly used to delay the execution of a piece of code (potentially time-consuming and resource-intensive) until it is actually needed, rather than executing it when the parameter is passed.
There is an example in the Swift standard library, the use of the Assert function. Since asserts are only triggered in debug mode, there is no need to execute the code in release mode. This is where @autoclosure comes in:
func assert(_ expression: @autoclosure() -> Bool,
_ message: @autoclosure() -> String) {
guard isDebug else {
return
}
// Inside assert we can refer to expression as a normal closure
if !expression() {
assertionFailure(message())
}
}
The above is my understanding of the implementation of assert, the actual implementation of assert is here
One advantage of @autoclosure is that it has no impact on the calling position. If assert is implemented using a "regular" closure, it would be used like this:
assert({ someConfition() }, { "Hey, it failed!" })
But now, it can be used as a function that accepts non-closure arguments:
assert(someCondition(), "Hey it failed!")
This week, let's take a look at how to use @autoclosure in our own code and how to use it to design elegant APIs.
Inline functions#
One use of @autoclosure is to inline expressions in function calls. This allows us to use a specified expression as an argument. Let's look at the following example:
In iOS, the following API is usually used to implement view animations:
UIView.animate(withDuration: 0.25) {
view.frame.origin.y = 100
}
Using @autoclosure, we can write an animate function that creates its own animation closure and executes it. Like this:
func animate(_ animation: @autoclosure @escaping () -> Void,
duration: TimeInterval = 0.25) {
UIView.animate(withDuration: duration, animations: animation)
}
Then, we can simply call a simple function without extra {} to call the animate function:
animate(view.frame.origin.y = 100)
With the above method, we reduce the length of the animation code without sacrificing readability.
Passing errors as expressions#
Another very useful feature of @autoclosure is when writing error handling utility classes. For example, if we want to unpack an Optional through a throwing API, we can add an extension to Optional to achieve this. In this case, we want the Optional to be non-nil, or throw an error, like this:
extension Optional {
func unwrapThrow(_ errorExpression: @autoclosure() -> Error) {
guard let value = self else {
throw errorExpression()
}
return value
}
}
Similar to the implementation of assert, this way we only check the error expression when needed, instead of checking it every time we unwrap the Optional. We can now use the unwrapOrThrow API like this:
let name = try argument(at: 1).unwrapOrThrow(ArgumentError.missingName)
Using default values for type inference#
The last use case of @autoclosure is when extracting a nullable value from a dictionary, database, or UserDefaults.
In general, to get a property from an uncertain type dictionary and provide a default value, you need to write the following code:
let coins = (dictionay["numberOfCoins"] as? Int) ?? 100
The above code is not only difficult to read, but also involves type inference and the ?? operator. We can use @autoclosure to define an API to achieve the same expression:
let coins = dictionary.value(forKey: "numberOfCoins", defaultValue: 100)
As we can see, defaultValue is used as both the default value when the value is missing and for type inference, without explicitly declaring the type or type capturing. Concise and clear👍
Then let's see how to define such an API:
extension Dictionary where Value = Any {
func value<T>(forKey key: Key, defaultValue: @autoclosure () -> T) -> T {
guard let value = self[key] as? T else {
return defaultValue()
}
return value
}
}
Similarly, we use @autoclosure to avoid checking defaultValue every time the method is executed.
Conclusion#
Reducing verbose code requires careful consideration. Our goal is to write self-explanatory and easy-to-read code, so when designing low-redundancy APIs, we need to make sure that important information is not removed from the call.
I think using @autoclosure appropriately is a great tool to achieve the above goals. Handling expressions, not just values, can not only reduce verbose code, but also potentially improve performance.
Do you think @autoclosure has other use cases? Or do you have any questions, comments, or feedback, please contact me. Twitter: @johnsundell
Thanks for reading! 🚀