- Proposal: SE-0073
- Authors: Félix Cloutier, Gwendal Roué
- Status: Rejected for Swift 3
- Review manager: Chris Lattner
This proposal introduces an optional once
argument to the @noescape
attribute. The @noescape(once)
attribute enforces that the closure does not
escape, and that it is run exactly once on any code path returning from the
function. For clients, it allows the compiler to relax initialization
requirements and close the gap between closure and "inline code" a little bit.
Swift-evolution thread: Guaranteed closure execution
In Swift, multiple functions execute a closure to temporarily grant code
special guarantees. For instance, the standard library's withUnsafePointer
affords the closure with a pointer to an object's representation, and the
autoreleasepool
function wraps a closure with code that creates (and later
destroys) an autorelease pool.
Currently, if you want to initialize a variable inside such a closure, you need to make it mutable and initially assign it a dummy value, because the compiler can't prove that the function will execute the closure exactly once. For instance:
var x: Int = 0 // `var` declaration, with some irrelevant value
f { x = 1 }
print(x)
By adding the @noescape(once)
attribute to the closure parameter, we tell the
compiler that the function will be executed exactly once on any code path that
leaves the function's scope:
func f(closure: @noescape(once) () -> ()) {
closure()
}
With this information, the compiler can now realize that the x
variable will
be written to exactly once. It can now be marked as a let
variable:
let x: Int // Not initialized
f { x = 1 }
print(x) // Guaranteed to be initialized
This new form is safer and cleaner.
@noescape(once)
can also be seen as a natural extension to SE-0061 in that we go from:
// Current Swift:
var x: Int = 0 // `var` declaration, with some irrelevant value
autoreleasepool {
x = 1
}
// Should SE-0061 be accepted:
let x = autoreleasepool {
return 1
}
// Should this proposal be accepted:
let x: Int
let y: String
autoreleasepool {
x = 1
y = "foo"
}
In addition to the regular advantages and constraints applied to @noescape
parameters, @noescape(once)
parameters must be called exactly once on any code
path where the function returns. Specifically:
- it must either be called or passed as a
@noescape(once)
parameter to another function on any code path that returns normally; - it must not be executed on any path that throws;
- there is no requirement for paths that lead to a function that does not return;
- passing it to another function that accepts a
@noescape(once)
closure of the same type is allowed and counts as executing it.
A @noescape(once)
closure may only read from variables that were initialized
before it was formed. For instance, in an example with two @noescape(once)
closures, the compiler cannot assume that one closure runs before the other.
func f(a: @noescape(once) () -> (), b: @noescape(once) () -> ()) { /* snip */ }
let x: Int
f(a: {x = 1}, b: {print(x)}) // invalid: x has not been initialized
A @noescape(once)
closure may only be passed as a parameter to another
function that accepts a @noescape(once)
closure. In that case, it counts as
having been called.
A @noescape(once)
closure may initialize let
or
var
variables from its parent scope as if it was executed at the call site.
Since SE-0049,
@noescape
is a type attribute. The @noescape(once)
modifier marks the
closure type as noescape.
This feature is purely additive. Existing code will continue to work as expected.
Assuming that the main goal of this proposal is to relax initialization requirements, a unique invocation of the closure is not stricly required. However the requirement of unique invocation makes the proposal simpler to understand.
It was mentioned in the discussion that the "once" behavior and @noescape
look
orthogonal, and the "once" behavior could be useful on closures that escape.
However, it is only possible to verify that a closure has been executed exactly
once if it does not escape. Because of this, "once" and @noescape
are better
left together.
It must either be guaranteed that the closure will be executed, or that it will not be executed, on a path that throws. It appears best to guarantee that it will not, since this allows the function to bail out without calling the closure in the event that it can't provide the guarantee that it's trying to get. For instance:
do {
let foo: Int
try withLock(someLock, timeout: 0.5) {
foo = sharedThing.foo
}
} catch {
print("couldn't acquire lock fast enough")
}
A function like this would be awkward to express if the closure had to test a parameter to tell if the lock was acquired or not.
As soon as a method does not provide the @noescape(once)
guarantee, it
prevents all functions that call it from providing it (see detailed design
above).
The value of this proposal will thus be hampered until methods and functions in
the standard and core libraries that can provide this guarantee adopt
@noescape(once)
.
This includes (full list to be done):
autoreleasepool
withUnsafeBufferPointer
dispatch_sync
et al.
Those modifications to standard and core libraries will however be part of future proposals.
On May 11, 2016, the core team decided to Reject this proposal for Swift 3.
The feedback on the proposal was generally positive both from the community and core team. That said, it is being rejected for Swift 3 two reasons:
-
The surface level syntax of @noescape needs to be reconsidered. At the minimum, we need to rename @noescape to @nonescaping for consistency with our previously agreed attribute naming scheme. However, it is also work discussing whether @nonescaping should be the default: if so, the attribute should actually become @escaping, and the functionality proposed in SE-0073 would be named @once.
-
Separate from the surface level issues, the implementation underlying this work has some significant challenges that are doable but would require major engineering work. Specifically, the definite initialization pass needs to “codegen” booleans in some cases for conditional initialization/overwrite cases, and these state values would have to be added to closure capture lists. This would require enough engineering work that it seems unlikely that it would happen in the Swift 3 timeframe, and beyond that this could theoretically be subsumed into a more general system that allowed control-flow-like functions to have closures that break/continue/throw/return out of their enclosing function, or a general macro system.
Overall, everyone desires the ability to produce more control-flow like functions, but Swift 3 isn’t in a place where it can make sense to tackle this work.