@once closure

Swift has a lot of functions that take a closure, and are known to call it exactly once.

Unsafe memory access:

func withUnsafePointer<T, Result>(to value: T, _ body: (UnsafePointer<T>) throws -> Result) rethrows -> Result
func withUnsafePointer<T, Result>(to value: inout T, _ body: (UnsafePointer<T>) throws -> Result) rethrows -> Result
func withUnsafeMutablePointer<T, Result>(to value: inout T, _ body: (UnsafeMutablePointer<T>) throws -> Result) rethrows -> Result
func withUnsafeBytes<T, Result>(of value: T, _ body: (UnsafeRawBufferPointer) throws -> Result) rethrows -> Result
func withUnsafeBytes<T, Result>(of value: inout T, _ body: (UnsafeRawBufferPointer) throws -> Result) rethrows -> Result
func withUnsafeMutableBytes<T, Result>(of value: inout T, _ body: (UnsafeMutableRawBufferPointer) throws -> Result) rethrows -> Result

Extending object’s lifetime:

func withExtendedLifetime<T, Result>(_ x: T, _ body: (T) throws -> Result) rethrows -> Result
func withExtendedLifetime<T, Result>(_ x: T, _ body: () throws -> Result) rethrows -> Result

Allow for the use of non-escaping closure in escaping one is required:

func withoutActuallyEscaping<ClosureType, ResultType>(_ closure: ClosureType, do body: (ClosureType) throws -> ResultType) rethrows -> ResultType

And Dispatch’s synchronization:

func sync<T>(flags: DispatchWorkItemFlags, execute work: () throws -> T) rethrows -> T

Sometimes you want to allocate data inside these closure but compiler won’t let you assign properties across closure boundary without capturing them.

let value: Int
queue.sync {
    // error
    // We need to capture `value` but value is not initialized.
    // Even if it’s initialized, we wouldn’t be able to assign value.
    value = 0 
}

This can be solved easily since most of these functions allow the closure to return values, and they’ll propagate those return values to the caller. The example then becomes:

let value = queue.sync { () -> Int in
    return 0 // ok
}

But this solution isn’t very scalable. If you want to initialize multiple variables, you’ll need to return tuple, and it can lose readability quickly.

let (value1, value2, value3, value4) = queue.sync { () -> (Int, Int, Int, Int) in
    // Are we returning items in the right order?
    // If there multiple return path, reordering/adding/removing variables will be troublesome.
    return (0, 0, 0, 0)
}

So I’d like for these closure to have the added @once annotation so that compiler can reason about these kind of closure more easily and potentially allow us to do this.

let value1, value2, value3, value4: Int
queue.sync {
    // some code
    value1 = 0
    // some other code
    value2 = 0
    // ...
    value3 = 0
    value4 = 0
}

So the @once closure will be treated much the same way as the do-block.

What do you guy’s think? Is there any limitation that prevent us from doing this?

5 Likes

Hello Lantua. This was the SE-0073 proposal: [Review] SE-0073: Marking closures as executing exactly once

It was rejected: [Rejected] SE-0073: Marking closures as executing exactly once

The main reason for rejection was the implementation challenges:

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.

Maybe the context has changed since then? I don't know.

4 Likes

Ah, thank you. I was trying to find if this has been proposed, but I didn’t find it.

I’ll look into it when I have time to see if it’s more plausible now.

Any idea if this could land after Swift 5? I don’t know how ABI stability will affect this.

I think one of the hurdle here is throwing closures.

let value: Int
someFunctionThatCallsAClosureOnce {
    if conditionA { throw a }
    value = 0 
    if conditionB { throw b }
}

This control flow here is quite difficult to manage. The closure was called, but value was perhaps not initialized. To fix this, the closure could be forced to initialize value on all paths or no paths, but never on half of them. That's a bit unintuitive though.

The rationale for rejecting SE-0073 mentions a preference for a more general system for control-flow-like functions that'd support throws, returns, breaks, and continues. @once hits a wall pretty quickly in the control flows it can support (like the one above) and the core team decided to wait for a better solution.

6 Likes

If you also required that someFunctionThatCallsAClosureOnce throws when its closure throws, then wouldn't that (with a try before the function call) be identical to

let value: Int
if conditionA { throw a }
value = 0
if conditionB { throw b }

Having the ability to constraint someFunctionThatCallsAClosureOnce this way would approach us to having those special control-flow-like functions. Add a few more constraints and an implementation capable of making sense of all this and you are pretty close to those ideal control-flow-like functions.