[swift-evolution] [Proposal Update 1] A simplified notation for avoiding the weak/strong dance with closure capture lists

David Turnbull dturnbull at gmail.com
Fri Feb 12 12:41:09 CST 2016


This could be the default behavior. A program can request "self", "weak
self", or "unowned self" if they want something else.

-david

On Fri, Feb 12, 2016 at 10:23 AM, Evan Maloney via swift-evolution <
swift-evolution at swift.org> wrote:

> Hello,
>
> I sent out an earlier draft of this proposal last week, and have updated
> it based on feedback from the list.
>
> The newer proposal now has a much smaller conceptual footprint to address
> concerns that the previous iteration was too complex and would lead to
> verbose notation within the capture lists.
>
> You can find the updated gist here
> <https://gist.github.com/emaloney/d34ac9b134ece7c60440>, as well as
> pasted below.
>
> Evan
>
> ---
>
> Simplified notation for avoiding the [weak self]/strongSelf dance with
> closures
>
>    - Proposal: TBD
>    - Author: Evan Maloney <https://github.com/emaloney>
>    - Status: *Draft*
>    - Review manager: TBD
>
> <https://gist.github.com/emaloney/d34ac9b134ece7c60440#introduction>
> Introduction
>
> Frequently, closures are used as completion callbacks for asynchronous
> operations, such as when dealing with network requests. It is quite common
> to model these sorts of operations in such a way that an object instance
> represents a request/response transaction, for example:
>
> protocol NetworkTransaction: class
> {
>     enum Result {
>         case Succeeded(NSData)
>         case Failed(ErrorType)
>     }
>
>     func execute(completion: (Result) -> Void)
> }
>
> Here, the NetworkTransaction protocol declares the interface by which an
> asynchronous transaction occurs. The user of a NetworkTransaction calls
> the execute() function, passing in a completion function that is called
> at some time in the future, when the transaction completes.
>
> For example, imagine a hypothetical DataConsumer class that uses a
> transaction to try to fetch some network data and process it:
>
> class DataConsumer
> {
>     let transaction: NetworkTransaction
>
>     init(transaction: NetworkTransaction)
>     {
>         self.transaction = transaction
>     }
>
>     func fetchData()
>     {
>         transaction.execute() { [weak self] result in
>             guard let strongSelf = self else {
>                 return
>             }
>
>             switch result {
>             case .Succeeded(let data):
>                 strongSelf.processData(data)
>
>             case .Failed(let err):
>                 strongSelf.handleError(err)
>             }
>         }
>     }
>
>     func processData(data: NSData)
>     {
>         // process the data
>     }
>
>     func handleError(error: ErrorType)
>     {
>         // handle the error
>     }
> }
>
> You'll notice the [weak self]/strongSelf dance in the fetchData() function.
> This is a common pattern with asynchronously-executed closures, and it
> signals the possibility that a closure might outlive its usefulness.
>
> Because the NetworkTransaction may complete at any time, it is possible
> that the closure will execute after the DataConsumer that initiated the
> transaction has been deallocated. Perhaps the user has navigated elsewhere
> in the application and whatever data was to be fetched by DataConsumer is
> no longer needed.
>
> In this case, after a DataConsumer instance goes away, we don't really
> want the closure doing anything. So, we capture self weakly to ensure
> that the closure doesn't hold a reference to the owning DataConsumer.
> That prevents a reference cycle and ensures that DataConsumer can be
> deallocated when no longer in use.
>
> When it comes time to execute the closure, the guard statement
> effectively asks the question, "Is self still alive?" If the answer is
> no, the guard forces a return and the rest of the closure does not execute.
>
> If self *is* still alive, then the weakly-captured self will be non-nil and
> it will be converted into a strong reference held by strongSelf for the
> duration of the closure's execution.
>
> When the closure is done executing, strongSelf goes away, once again
> making the DataConsumer eligible for deallocation when no other
> references are held.
> <https://gist.github.com/emaloney/d34ac9b134ece7c60440#the-problem>The
> Problem
>
> The [weak self]/strongSelf dance requires common boilerplate wherever it
> is used, and the fact that a self-like variable with an arbitrary name
> adds noise within the closure. The more strongSelf is needed within the
> closure, the more noise there is.
>
> Further, using a consistent name like strongSelf is by convention only;
> it can't be enforced by the compiler, so searching your codebase for a
> given keyword won't be exhaustive if team members use the wrong name.
> <https://gist.github.com/emaloney/d34ac9b134ece7c60440#proposed-solution>Proposed
> Solution
>
> The proposed solution adds a new capture type by repurposing the guard keyword
> for another use, which would look like:
>
> transaction.execute() { [guard self] result in
>     switch result {
>     case .Succeeded(let data):
>         self.processData(data)
>
>     case .Failed(let err):
>         self.handleError(err)
>     }
> }
>
> Here, the [guard self] capture list serves as a signal that the compiler
> should handle the weak/strong dance itself. When encountering [guard self],
> the compiler should emit code that does the following:
>
>    - Captures self in a weak reference on behalf of the closure
>    - Whenever the closure is about to be executed, the weak reference is
>    checked to see if self is still alive
>       - If self is not alive, the closure becomes a no-op
>       <https://en.wikipedia.org/wiki/NOP>; calling the closure returns
>       immediately without anything inside the braces being executed
>       - If self is alive, it is upgraded to a strong reference for the
>       lifetime of the closure's execution. Within the closure, self is
>       non-optional, unlike how it would be with a [weak self] capture.
>       When the closure is done executing, the strong reference will be cleared
>       and only the weak reference will be held on behalf of the closure.
>
> <https://gist.github.com/emaloney/d34ac9b134ece7c60440#non-self-references>
> Non-self References
>
> Because guard is an additional capture type, like weak and unowned, it
> can also be used to capture references other than self:
>
> let capturingTwo = { [guard self, button] in
>     // weakly capture self and button
>     // but execute the closure with strong references
>     // if and only if self AND button still exist
>     // when the closure is being asked to execute
> }
>
> When encountering multiple references being captured via guard, the
> closure will execute *only* when *all* references are still alive when
> the closure is being asked to execute.
> <https://gist.github.com/emaloney/d34ac9b134ece7c60440#limitations>
> Limitations
>
> Because guard is a special capture type that causes the closure to become
> a no-op once a referenced object deallocates, it is only designed to be
> used with closures returning Void.
>
> This limitation was deemed acceptable because it would cover the vast
> majority of cases, and those that it didn't cover can still fall back on
> the existing technique.
>
> The compiler should emit an error if this notation is used in conjunction
> with a closure that has a non-Void return type.
> <https://gist.github.com/emaloney/d34ac9b134ece7c60440#caveats>Caveats
>
> This notation is not intended to be a full-fledged replacement for guard
> statements within the closure. We are only using guard here as a way to
> declare a specific memory-management behavior for references. Therefore,
> guard within [square brackets ] should be seen as a capture type on par
> with weak or unowned.
>
> Unlike with a typical guard statement, we are not attempting to support
> an else or where clause, or any boolean expressions within this notation.
>
> Rather, we're simply adding a new capture behavior and providing a means
> to specify an early exit if the behavior couldn't be fulfilled because one
> or more objects was deallocated.
>
> The word guard was chosen as the capture type because (1) it functions as
> a guard, ensuring that the closure doesn't execute unless the specified
> objects are still alive, and (2) it obviates the need for the full-fledged
> guard statement that would otherwise be required to achieve the same
> result.
>
> <https://gist.github.com/emaloney/d34ac9b134ece7c60440#impact-on-existing-code>Impact
> on Existing Code
>
> None, since this does not affect any existing constructs. Implementation
> of this proposal will not result in any code breakage.
>
> <https://gist.github.com/emaloney/d34ac9b134ece7c60440#alternatives-considered>Alternatives
> Considered
> <https://gist.github.com/emaloney/d34ac9b134ece7c60440#status-quo>Status
> Quo
>
> The primary alternative is to do nothing, requiring developers to add
> boilerplate guard code and handle upgrading the weak-to-strong references
> manually.
>
> As stated above, this leads to needless boilerplate that can easily be
> factored out by the compiler. Also, the use of a self-like variable with
> an arbitrary name makes it more difficult to exhaustively find such uses in
> large projects. With this proposal, searching for the text "[guard" is
> all that's necessary to find all instances of this memory management
> technique.
>
> Finally, the need to declare and use alternate names to capture values
> that already have existing names adds visual clutter to code and serves to
> obscure the code's original intent, making it harder to reason about.
>
> <https://gist.github.com/emaloney/d34ac9b134ece7c60440#closures-with-an-optional-return-type>Closures
> with an Optional Return Type
>
> One possible addition to this proposal would extend support to any closure
> returning an Optional of some kind.
>
> If one of the objects in a guard capture list has been deallocated,
> executing the closure will always result in an immediate nil return.
>
> This idea was excluded from this iteration of the proposal due to a
> concern that it relied on a "magic return value" (albeit a reasonable one)
> and a perception that the community favored a solution with a smaller
> conceptual footprint.
>
> <https://gist.github.com/emaloney/d34ac9b134ece7c60440#closures-with-a-bool-return-type>Closures
> with a Bool Return Type
>
> One possible addition to this proposal would extend support to any closure
> returning a Bool.
>
> If one of the objects in a guard capture list has been deallocated,
> executing the closure will always result in an immediate false return.
>
> This idea was excluded from this iteration of the proposal due to a
> concern that it relied on a "magic return value" (albeit a reasonable one)
> and a perception that the community favored a solution with a smaller
> conceptual footprint.
>
> <https://gist.github.com/emaloney/d34ac9b134ece7c60440#closures-with-arbitrary-return-types>Closures
> with Arbitrary Return Types
>
> An earlier iteration of this proposal included support for closures with
> arbitrary return values. The community consensus was that the proposal was
> too heavy-weight and tried to do too much, and would lead to verbosity
> within the capture declaration. As a result, this idea was removed from the
> proposal.
>
> The ability to handle non-Void return values relied on supporting an else clause
> within a guard-based capture list:
>
> let happinessLevel: () -> Int = { [guard self else -1] in
>     var level = 0
>     level += self.isHealthy ? 25 : 0
>     level += !self.isHungry ? 25 : 0
>     level += !self.isFearful ? 25 : 0
>     level += self.hasLove ? 25 : 0
>     return level
> }
>
> Here, the else clause provides a value to return in cases where self has
> gone away and the guard fails.
>
> In this example, if you call happinessLevel() after self has been
> deallocated, the value -1 will always be returned.
> <https://gist.github.com/emaloney/d34ac9b134ece7c60440#citations>Citations
>
> Variations on this proposal were discussed earlier in the following
> swift-evolution <https://lists.swift.org/mailman/listinfo/swift-evolution>
>  threads:
>
>    - Wanted: syntactic sugar for [weak self] callbacks
>    <https://lists.swift.org/pipermail/swift-evolution/Week-of-Mon-20160201/008713.html>
>    - Allowing guard let self = self else { … } for weakly captured self
>    in a closure.
>    <https://lists.swift.org/pipermail/swift-evolution/Week-of-Mon-20160201/009023.html>
>    - [Draft Proposal] A simplified notation for avoiding the weak/strong
>    dance with closure capture lists
>    <https://lists.swift.org/pipermail/swift-evolution/Week-of-Mon-20160201/009241.html>
>
>
>
> _______________________________________________
> 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/20160212/bac88a09/attachment.html>


More information about the swift-evolution mailing list