Building assert() in Swift, Part 1: Lazy Evaluation

When designing Swift we made a key decision to do away with the C preprocessor, eliminating bugs and making code much easier to understand. This is a big win for developers, but it also means Swift needs to implement some old features in new ways. Most of these features are obvious (importing modules, conditional compilation), but perhaps the most interesting one is how Swift supports macros like assert().

When building for release in C, the assert() macro has no runtime performance impact because it doesn’t evaluate any arguments. One popular implementation in C looks like this:

#ifdef NDEBUG #define assert(e) ((void) 0 ) #else #define assert(e) \ ((void) ((e) ? ((void) 0 ) : __assert (#e, __FILE__, __LINE__))) #define __assert(e, file, line) \ ((void)printf ( "%s:%u: failed assertion `%s'

" , file, line, e), abort()) #endif

Swift’s assert analog provides almost all of the functionality of C’s assert, without using the preprocessor, and in a much cleaner way. Let’s dive in and learn about some interesting features of Swift.

Lazy Evaluation of Arguments

When implementing assert() in Swift, the first challenge we encounter is that there is no obvious way for a function to accept an expression without evaluating it. For example, say we tried to use:

func assert(x : Bool ) { #if !NDEBUG #endif }

Even when assertions are disabled, the application would take the performance hit of evaluating the expression:

assert(someExpensiveComputation() != 42 )

One way we could fix this is by changing the definition of assert to take a closure:

func assert(predicate : () -> Bool ) { #if !NDEBUG if !predicate() { abort() } #endif }

This evaluates the expression only when assertions are enabled, like we want, but it leaves us with an unfortunate calling syntax:

assert({ someExpensiveComputation() != 42 })

We can fix this by using the Swift @autoclosure attribute. The auto-closure attribute can be used on an argument to a function to indicate that an unadorned expression should be implicitly wrapped in a closure to the function. The example then looks like this:

func assert(predicate : @autoclosure () -> Bool ) { #if !NDEBUG if !predicate() { abort() } #endif }

This allows you to call it naturally, as in:

assert(someExpensiveComputation() != 42 )

Auto-closures are a powerful feature because you can conditionally evaluate an expression, evaluate it many times, and use the bound expression in any way a closure can be used. Auto-closures are used in other places in Swift as well. For example, the implementation of short-circuiting logical operators looks like this:

func &&(lhs: BooleanType , rhs: @autoclosure () -> BooleanType ) -> Bool { return lhs. boolValue ? rhs(). boolValue : false }

By taking the right side of the expression as an auto-closure, Swift provides proper lazy evaluation of that subexpression.

Auto-Closures

As with macros in C, auto-closures are a very powerful feature that must be used carefully because there is no indication on the caller side that argument evaluation is affected. Auto-closures are intentionally limited to only take an empty argument list, and you shouldn’t use them in cases that feel like control flow. Use them when they provide useful semantics that people would expect (perhaps for a “futures” API) but don’t use them just to optimize out the braces on closures.

This covers one special aspect of the implementation of assert in Swift, but there is more to come.

All Blog Posts