Swift - @autoclosure

While closures are a powerful way to capture and store references within a specific context, the heavy-weight syntax using curly brackets may sometimes lead to confusion and code misinterpretation.

The @autoclosure attribute tells the system to automatically wrap the function’s argument as a closure enabling us to omit these brackets at function call.

An autoclosure is a closure that is automatically created to wrap an expression that’s being passed as an argument to a function.

It doesn’t take any arguments, and when it’s called, it returns the value of the expression that’s wrapped inside of it.

This syntactic convenience lets you omit braces around a function’s parameter by writing a normal expression instead of an explicit closure.

The Swift standard library uses the @autoclosure attribute in well-known methods such as the assert(condition:message:file:line:) or precondition(condition:message:file:line:) ones using below syntax.

public func precondition(
    _ condition: @autoclosure () -> Bool, 
    _ message: @autoclosure () -> String = String(), 
    file: StaticString = #file, 
    line: UInt = #line
)

Calling the precondition method with an @autoclosure attribute is straightforward.

precondition(3 > 2, "Where do you think you're going?")

Both of the 3 > 2 and String message expressions will be automatically wrapped in a closure providing some syntactic sugar. Creating an Int extension with a mutating increase function gives us the ability to increase a given value with a random passed-in operation argument.

extension Int {
    mutating func increase(_ operation: @autoclosure () -> Int) {
        precondition(
            (self + operation()) > self, 
            "Decreasing a value is not allowed"
        )
        self += operation()
    }
}

var number = 0

number.increase(2*3) // prints 6
number.increase(3+2) // prints 11
number.increase(18) // Error: Decreasing a value is not allowed

Using the @autoclosure attribute comes in handy whenever we need to apply some transformations to an array using a single function call.

extension Array {
    func applied<T>(_ operation: @autoclosure () -> [T]) -> [T] {
        return operation()
    }
}
var numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

let evens = numbers.applied(numbers.filter { $0.isMultiple(of: 2) })
let doubles = numbers.applied(numbers.map { $0 * 2 })
let strings = numbers.applied(numbers.map(String.init))

Implementing the following removed function allows to gather all the removal operations under a single function.

extension Array {
    @discardableResult
    func removed(_ operation: @autoclosure () -> (Self.Element)) -> Self.Element {
        return operation()
     }
}

let n1 = numbers.removed(numbers.remove(at: 2)) // prints 3
let n2 = numbers.removed(numbers.removeFirst()) // prints 1
let n3 = numbers.removed(numbers.removeLast()) // prints 10

While the @autoclosure attribute improves code readability, it also makes debugging easier as we have to deal with a one and only function throughout our program.

The nice thing with @autoclosure is that it enables us to defer the assertion of conditions — just like in the assert() or precondition() functions — preventing our program to start potentially expensive operations.

func isValid(_ lhs: Bool, _ rhs: @autoclosure () -> Bool) -> Bool {
    guard lhs else {
        return false
    }
    return rhs()
}
if isValid(numbers.count > 0, numbers.last == 10) {
    // perform some work
}

The rhs argument will only be executed if lhs is true.

As mentioned above, deferring expensive computations using the @autoclosure attribute may result in valuable program optimization.

Conclusion

The @autoclosure attribute is a convenient way to improve code legibility by passing a closure as an argument.

The system handles the wrapping of that closure which can then be called within the body enabling us to defer the assertion of conditions if necessary.

Using a single function to handle many operations such as map(), filter() leads to better code maintenance and faster debugging.

Beware though that an overuse of the @autoclosure attribute may also lead to confusion and misunderstanding.