Subscribe to Jacob’s Tech Tavern for free to get ludicrously in-depth articles on iOS, Swift, tech, & indie projects in your inbox every two weeks.
Paid subscribers unlock Quick Hacks, my advanced tips series, and enjoy exclusive early access to my long-form articles.
It’s not every day a meme gives you imposter syndrome, but this did.
While I recognise many of these words, I frankly didn’t know what most of these actually do. I don’t even know what these things are called.
It turns out the term is “type attributes”.
The best way to understand type attributes is by checking how they’re implemented in the Swift source code.
Ha, just kidding, I’ll explain them here.
Type attributes provide the compiler with additional information about a type. When used in a function, they enforce constraints on the type they are applied to — in many cases, this type is a closure: () -> Void
.
Overcoming Imposter Syndrome
Today, we’re going to work together to understand type attributes in detail by recreating the meme step-by-step — you’re welcome to open a Swift playground and code along.
Beginning, naturally, with hello world.
func allTheAttributes(_ then: () -> Void) {
then()
}
allTheAttributes {
print("Hello, world!")
}
The then
closure executes immediately, and so there’s no problem passing it in directly as a simple unadorned closure.
But what if the closure wasn’t immediately executed?
@escaping
This attribute indicates that a type in a method declaration can be stored for later execution. Therefore, a closure marked @escaping
can outlive the lifetime of the function itself — it “escapes” the function scope, stored to be executed later.
Due to outliving the call site, escaping closures are a common source of memory leaks, which is why you’ll often be capturing [weak self]
(or unowned self) to avoid this.
@escaping
is only required for closures in function parameter position, i.e. closures directly used as arguments. If you wrapped this closure with an enum’s associated value (this happens every time you use an optional!), then it becomes implicitly escaping. This is why optional closures like (() -> Void)?
don’t need to be @escaping
.
Let’s update our function so that the closure can execute after the method finishes, by placing the () -> Void
inside some other closure.
func allTheAttributes(_ then: () -> Void) {
Task {
then()
}
}
Now, we get a compiler warning:
Escaping closure captures non-escaping parameter 'then'
To address this, we can add the @escaping
attribute to the closure type () -> Void
and get our code compiling again!
func allTheAttributes(_ then: @escaping () -> Void) {
Task {
then()
}
}
allTheAttributes {
print("Hello, world!")
}
@Sendable
Sendable might be the most wilfully misunderstood parts of Swift Concurrency.
We get it, you’re a compiler warning, shut up, I’ll fix you in September.
When a type is marked @Sendable
, this means it’s thread-safe. Sendable types can be passed across arbitrary concurrency contexts without risking data races.
When a closure type is marked @Sendable
, this means any values passed into the closure must also be Sendable, and can’t be captured by reference. Since a Sendable closure captures its parameters by value, it eliminates the possibility of corrupting data with conflicting data mutations on parallel threads.
If we turn on strict concurrency checking, we get a warning on our then()
closure:
Capture of 'then' with non-sendable type '() -> Void' in a `@Sendable` closure
This is because the initialiser on Task
itself takes a @Sendable closure:
public init(priority: TaskPriority? = nil, operation: @escaping @Sendable () async -> Success)
Now we’re equipped with the tools to handle this error: we can simply tell the compiler that our own closure is also @Sendable
, making it possible to pass into Task
.
func allTheAttributes(_ then: @escaping @Sendable () -> Void) {
Task {
then()
}
}
allTheAttributes {
print("Hello, world!")
}
And the compiler warning is thusly satisfied (until Swift 7 at least).
@MainActor
The Main Actor is analogous to the main queue, a global entity on which all UI work is performed.
Actors guarantee serial access to mutable state, and prevent multiple threads from executing code on them in parallel. They do this using a special internal queue known as a serial executor. (If you’re unfamiliar, you can find a deeper intro to actors here).
When used as a type attribute in a function argument, @MainActor
ensures the argument type is isolated to the main actor — this is really helpful for anything that touches the UI.
If we wanted to refactor our application so that print(“Hello world!”)
was executed on the main thread, we could write something like this:
func allTheAttributes(_ then: @escaping @Sendable () -> Void) {
Task {
then()
}
}
@MainActor func helloWorld() {
print("Hello, world!")
}
allTheAttributes {
helloWorld()
}
But now we get another compiler error:
Call to main actor-isolated global function 'helloWorld()' in a synchronous nonisolated context
You might have guessed the solution by now.
We decorate the argument in allTheAttributes
with @MainActor
, and since we might need to wait before executing UI work, await
the closure before running it.
func allTheAttributes(_ then: @escaping @Sendable @MainActor () -> Void) {
Task {
await then()
}
}
@autoclosure
This attribute is pretty rare to see, but it’s conceptually pretty simple: it automatically (automagically?) wraps an expression in a closure.
For examples, look no further than the Swift Standard Library. We can see real-life use cases such as the famous nil coalescing operator:
public func ?? <T: ~Copyable>(
optional: consuming T?,
defaultValue: @autoclosure () throws -> T
) rethrows -> T
In this instance, the default value is attributed with @autoclosure
, so that you can coalesce with the result of a function:
var optionalNumber: Int?
var actualNumber: Int = optionalNumber ?? defaultNumber()
func defaultNumber() -> Int {
return 1
}
An added bonus of the @autoclosure
attribute is that evaluation of the argument is performed lazily. Therefore, if the nil coalescing operation doesn’t need the defaultValue
, then the closure is never called and some clock cycles are saved.
If this code is tricky to understand, we can create a version of the nil coalescing operator without the autoclosure in the function signature:
func noAutoClosure<T>(
_ optional: T?,
_ defaultValue: () throws -> T
) rethrows -> T
var actualNumber: Int = noAutoClosure(optionalNumber, defaultNumber())
We get a compiler warning:
Cannot convert value of type 'Int' to expected argument type '() throws -> Int'
To get our fallback to work without @autoclosure
, we’d need to put the default number in a raw closure. Yuck!
var actualNumber: Int = noAuto(optionalNumber) {
defaultNumber()
}
Therefore, @autoclosure
can be really useful for simplifying API in some places, however it’s easy to see how you can make your code pretty hard to reason about if you overuse it.
Let’s take our victory lap and craft the final form of our function signature.
The @autoclosure
attribute really didn’t play well with the @MainActor
attribute, giving several compiler warnings:
Call to main actor-isolated global function 'helloWorld()' in a synchronous nonisolated context
Playing some whack-a-mole with async
and await
keywords, I finally arrive at a final method which the compiler is happy with, including our ultimate function signature.
Task {
await allTheAttributes(await helloWorld())
}
func allTheAttributes(
_ then: @escaping @Sendable @MainActor @autoclosure () async -> Void
) async {
Task {
await then()
}
}
@MainActor func helloWorld() {
print("Hello, world!")
}
Now we can die happy, basking in the beauty of the ultimate function signature:
@escaping @Sendable @MainActor @autoclosure () async -> Void
Conclusion
Don’t try this at home, kids!
There’s a time and place for putting type attributes in your functions, and it’s extremely useful to have them on your toolbelt.
However, if you find yourself reaching for @escaping @Sendable @MainActor @autoclosure () async -> Void
outside the context of a meme, a blog post, or a lunch & learn, think about whether you’re trying too hard to be clever.
If you’re thirsty for more, you can read the docs or even check out how they’re implemented in the Swift source code.
Good post! I would just add that for me, `@autoclosure` is best reserved for situations where you have a parameter value that a) could be expensive and b) may never need to be executed.
The nil-coalescing example is a good one, in that if the optional value you're coalescing is not nil, the remainder of the expression never need to execute.
Another classic example is `assert`. For example:
```
assert(someValue == someOtherValue, "Expected \(someOtherValue) but got \(someValue).")
```
Both the condition being tested and the message have a processing cost. If we're not running in debug mode, assertions always pass, so processing them would be less efficient. And the message will only be output when the assertion fails.
So, this is where autoclosure is useful - it makes it simple to wrap up code that may not need to execute.