[swift-evolution] Pitch: @required attribute for closures

Andrew Bennett cacoyi at gmail.com
Sun Jun 5 18:50:18 CDT 2016


Perhaps I was unclear, in my explanation. The guarantee I'm enforcing is
that the closure is called exactly once before being released.

Everything I suggested is a compile-time check.

The compile-time warning and runtime `fatalError` I suggested could be
replaced with a compile-time error, however even in this case it is still
statically checked for the warning.

The compiler can statically guarantee *exactly one* of these things happens
in methods using the closure:

   - the closure is *called*
   - the closure is *stored*
   - the closure is *passed* to another method
   - the program *aborts* with something like a fatalError

If the closure is stored then there must be a *deinit*, and those checks
apply there as well.

I believe this is sufficient to ensure the closure is called once. Please
let me know if there are any cases these checks miss.

On Sun, Jun 5, 2016 at 11:59 PM, Matthew Johnson <matthew at anandabits.com>
wrote:

>
>
> Sent from my iPad
>
> On Jun 5, 2016, at 8:52 AM, Andrew Bennett <cacoyi at gmail.com> wrote:
>
> Storing into a member would be fine, as long as it must keep @once as a
> type annotation and the compiler makes sure you maintain:
>     sum(callCount, storeCount, passCount) == 1
>
> For example:
>   class Example {
>     private var closure: (@once (T) -> Void)?
>
>     func callClosure(value: T, replace: (@once (T) -> Void)? = nil) {
>
>       // the compiler should error if it detects the closure:
>
>       //  * escaping more than once, while still being stored,
>
>       //  * or being called while still being stored or escaping,
>
>       //  * or being overwritten without being called
>
>       if let closure = self.closure {
>
>         self.closure = replace
>
>         closure(value)
>
>       }
>
>     }
>
>
>     deinit {
>
>       // compiler warning: that closure is potentially un-called
>
>       // runtime fatalError if it's .Some(Closure) after deinit
>
>     }
>
>   }
>
> There could be a standard library type with those guarantees built in.
>
>
> I don't consider this compiler verification.  It is runtime verification.
> The best the compiler can do is enforce constraints that allow for
> guaranteed runtime verification.  You can argue that is better than nothing
> but it is not a static guarantee of correct behavior.
>
>
>
> On Sun, Jun 5, 2016 at 10:12 PM, Matthew Johnson <matthew at anandabits.com>
> wrote:
>
>>
>>
>> Sent from my iPad
>>
>> On Jun 5, 2016, at 6:56 AM, Andrew Bennett <cacoyi at gmail.com> wrote:
>>
>> I like this.
>>
>> One of the suggestions on @noescape(once) was that it just becomes @once
>> and works with escaping closures too. It might be possible if compile time
>> checks verified that the closure isn't copied, and that it is called before
>> being deinit-ialized. Failing that I'm happy with a runtime circumstance in
>> the cases the compiler can't check.
>>
>>
>> Yeah, maybe if it is only used asynchronously and never stored in a
>> member or global it could be verified and that is a pretty common case.
>> That would certainly be easier than the general case.
>>
>> I prefer @once over @required if the guarantee is single execution.  If
>> the guarantee is *at least once* obviously @once is not the right
>> attribute, but I'm not convinced @required is either.  Maybe @invoked.
>>
>>
>> It would be great if @required took into the account the feedback from
>> that proposal and considered the synchronous case too.
>>
>> As an aside, you can get some of the guarantees you want like this:
>>
>> func doSomething(completionHandler: (SomeEnum) -> ()) {
>>
>>   dispatch_async(someQueue) {
>>
>>     let result: SomeEnum
>>
>>     // the compiler ensures 'result' is set
>>
>>     defer { completionHandler(result) }
>>
>>
>>     if aCondition {
>>
>>       if bCondition {
>>
>>         result = .Foo
>>
>>       } else {
>>
>>         result = .Bar
>>
>>       }
>>
>>       // the compiler ensures you do this, because it is 'let'
>>
>>       return
>>     }
>>
>>
>>     if cCondition {
>>
>>       result = .Baz
>>
>>     }
>>
>>   }
>>
>> }
>>
>> On Sun, Jun 5, 2016 at 9:42 PM, Matthew Johnson via swift-evolution <
>> swift-evolution at swift.org> wrote:
>>
>>>
>>>
>>> Sent from my iPad
>>>
>>> On Jun 5, 2016, at 5:02 AM, Patrick Pijnappel via swift-evolution <
>>> swift-evolution at swift.org> wrote:
>>>
>>> This has actually been proposed before, see SE-0073:
>>> https://github.com/apple/swift-evolution/blob/master/proposals/0073-noescape-once.md
>>>
>>>
>>> Actually that proposal was for noescape closures and this suggestion is
>>> for escaping closures.  I don't think the compiler can verify this for
>>> noescape closures.  If it is possible it would be far more complicated.
>>>
>>>
>>>
>>> On Sun, Jun 5, 2016 at 11:37 AM, Charles Srstka via swift-evolution <
>>> swift-evolution at swift.org> wrote:
>>>
>>>> MOTIVATION:
>>>>
>>>> As per the current situation, there is a pitfall when writing
>>>> asynchronous APIs that does not occur when writing synchronous APIs.
>>>> Consider the following synchronous API:
>>>>
>>>> func doSomething() -> SomeEnum {
>>>>         if aCondition {
>>>>                 if bCondition {
>>>>                         return .Foo
>>>>                 } else {
>>>>                         return .Bar
>>>>                 }
>>>>         } else {
>>>>                 if cCondition {
>>>>                         return .Baz
>>>>                 }
>>>>         }
>>>> }
>>>>
>>>> The compiler will give an error here, since if both aCondition and
>>>> cCondition are false, the function will not return anything.
>>>>
>>>> However, consider the equivalent async API:
>>>>
>>>> func doSomething(completionHandler: (SomeEnum) -> ()) {
>>>>         dispatch_async(someQueue) {
>>>>                 if aCondition {
>>>>                         if bCondition {
>>>>                                 completionHandler(.Foo)
>>>>                         } else {
>>>>                                 completionHandler(.Bar)
>>>>                         }
>>>>                 } else {
>>>>                         if cCondition {
>>>>                                 completionHandler(.Baz)
>>>>                         }
>>>>                 }
>>>>         }
>>>> }
>>>>
>>>> Whoops, now the function can return without ever firing its completion
>>>> handler, and the problem might not be discovered until runtime (and,
>>>> depending on the complexity of the function, may be hard to find).
>>>>
>>>> PROPOSED SOLUTION:
>>>>
>>>> Add a @required attribute that can be applied to closure arguments.
>>>> This attribute simply states that the given closure will always be
>>>> eventually called, and the compiler can enforce this.
>>>>
>>>> DETAILED DESIGN:
>>>>
>>>> - The @required attribute states in our API contract that a given
>>>> closure *must* be called at some point after the function is called.
>>>>
>>>> - Standard API calls like dispatch_async that contractually promise to
>>>> execute a closure or block get @required added to their signatures.
>>>>
>>>> - When the compiler sees a @required closure in a function declaration,
>>>> it checks to make sure that every execution path either calls the closure
>>>> at some point, or sends a @required closure to another API that eventually
>>>> ends up calling the closure.
>>>>
>>>> - If there’s a way for a @required closure not to be called, the
>>>> compiler emits an error letting the developer know about the bug in his/her
>>>> code.
>>>>
>>>> IMPACT ON EXISTING CODE:
>>>>
>>>> None. This is purely additive.
>>>>
>>>> ALTERNATIVES CONSIDERED:
>>>>
>>>> I got nothin’.
>>>>
>>>> Charles
>>>>
>>>> _______________________________________________
>>>> swift-evolution mailing list
>>>> swift-evolution at swift.org
>>>> https://lists.swift.org/mailman/listinfo/swift-evolution
>>>>
>>>
>>> _______________________________________________
>>> swift-evolution mailing list
>>> swift-evolution at swift.org
>>> https://lists.swift.org/mailman/listinfo/swift-evolution
>>>
>>>
>>> _______________________________________________
>>> swift-evolution mailing list
>>> swift-evolution at swift.org
>>> https://lists.swift.org/mailman/listinfo/swift-evolution
>>>
>>>
>>
>
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <https://lists.swift.org/pipermail/swift-evolution/attachments/20160606/937aaea7/attachment.html>


More information about the swift-evolution mailing list