[swift-evolution] Guaranteed closure execution

Félix Cloutier felixcca at yahoo.ca
Sun Jan 31 21:39:56 CST 2016


To simplify my explanations, let's have these functions:

> func foo() {
> 	let bar: Int
> 	withNoEscape { bar = 1 }
> }
> 
> func withNoEscape(@autoclosure(once) closure: () -> ()) { /* snip */ }


Looking back, I do think that there should be a way to exit from `withNoEscape` without calling the closure, so yes, throwing should imply that the closure wasn't executed. If it's possible that `foo` swallowed an error from a throwing `withNoEscape`, the compiler should assume that the variables within haven't been initialized:

> func foo() {
> 	let bar: Int
> 	do {
> 		try withNoEscape { bar = 1 }
> 		// bar has been initialized
> 	}
> 	catch {
> 		// bar has not been initialized
> 	}
> }
> 
> func foo() {
> 	let bar: Int
> 	try? withNoEscape { bar = 1 }
> 	// bar has not been initialized
> }
> 
> func foo() {
> 	let bar: Int
> 	try! withNoEscape { bar = 1 }
> 	// bar has been initialized
> }

Closures that can throw are harder since normal functions are allowed to catch an error from a closure. For a @noescape(once) closure, it has been partially executed only and it would be non-trivial to determine what has been executed and what hasn't. As I see it, the best solution would be to force any code path in `withNoEscape` where it catches an exception from the closure to throw to indicate failure (even if the closure partially ran).

Another solution is to prevent `withNoEscape` from throwing, and prevent `@noescape(once)` closures from throwing. It's less classy, but the current places in the standard library where it would be nice to have it do not throw and I have a hunch that most cases won't need to throw either. Recovering from throwing closures does add a lot of complexity that I'm not sure the feature will pay for in the long run.

Félix

> Le 31 janv. 2016 à 12:54:27, Dany St-Amant via swift-evolution <swift-evolution at swift.org> a écrit :
> 
>> 
>> Le 31 janv. 2016 à 01:17, Félix Cloutier via swift-evolution <swift-evolution at swift.org <mailto:swift-evolution at swift.org>> a écrit :
>> 
>> I'd like to pitch this proposal to implement the feature: https://github.com/zneak/swift-evolution/blob/master/proposals/00xx-noescape-once.md <https://github.com/zneak/swift-evolution/blob/master/proposals/00xx-noescape-once.md>
>> 
>> Rationale for some points:
>> 
>>> Only one closure parameter can be marked as @noescape(once) in a function signature.
>> 
>> 
>> The attribute doesn't specify the order of execution of the closures, so it could have unintended consequences if closure B depends on closure A but closure B is called first. Given the typical use case (the @noescape(once) closure as a trailing closure), I don't think that it's worth it to invest a lot of effort into coming up with a model to decide (and enforce) which closure has to be called first and what to do if either closure throws or something.
>> 
>>> it is not required to be executed on a code path that throws;
>> 
>> 
>> It may need to be clarified into "must" or "must not", but I can't think about very good examples supporting either case right now.
>> 
> 
> What is the implication of this @noescape(once) closure not being call on throws when the caller use try? variation? Looks like the compiler will have to assume that the @escape(once) is both not called and not not called (sorry for the double-negative). For the try!, i think that the compiler could assume that the @escape(none) is called, as the process would crash otherwise anyway on the throw.
> 
>     var bad: Bool = true
>     enum e: ErrorType {
>         case Simple
>     }
>     func f(@noescape closure: () -> ()) throws {
>         if (bad) { throw e.Simple }
>         closure()
>     }
>     let x: Int  // Not initialized
>     try? f { x = 1 }
>     print(x)    // May still be uninitialized, compiler must generate error
>     x = 2       // May have been initialized, compiler must generate error
> 
> This should probably be highlighted in the proposal as an intended limitation for the typical usage.
> Another special case with try? that people may be unlikely to use, is:
> 
>     func g(@noescape closure: () -> ()) throws -> Int {
>         if (bad) { throw e.Simple }
>         closure()
>         return 1
>     }
>     if let data = try? g({ x = 1 }) {
>         print(x) // Guaranteed to be initialized
>     }
>     print(x)    // May still be uninitialized, compiler must generate error
>     x = 2       // May have been initialized, compiler must generate error
> 
> Not sure if this case will make the implementation more complex, it is why I’m mentioning it.
> 
> Dany
> _______________________________________________
> swift-evolution mailing list
> swift-evolution at swift.org <mailto:swift-evolution at swift.org>
> https://lists.swift.org/mailman/listinfo/swift-evolution <https://lists.swift.org/mailman/listinfo/swift-evolution>
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <https://lists.swift.org/pipermail/swift-evolution/attachments/20160131/10d7db42/attachment.html>


More information about the swift-evolution mailing list