[swift-evolution] [Proposal Update 1] A simplified notation for avoiding the weak/strong dance with closure capture lists
Andrew Bennett
cacoyi at gmail.com
Sat Feb 13 02:45:16 CST 2016
Looks great, I support this as-is, I do have one suggestion, perhaps for a
follow up proposal:
Have a protocol akin to NilLiteralConvertible, let's call it
GuardFailureConvertible for now. Then have any failing guard able to
implicitly return that value. The protocol is something like this:
protocol GuardFailureConvertible {
init(guardFailure: Void)
}
Your guard syntax could go unchanged, normal guards could also have a
shorthand:
guard something() else return
On Saturday, 13 February 2016, 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
>
>
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <https://lists.swift.org/pipermail/swift-evolution/attachments/20160213/b530b7d1/attachment-0001.html>
More information about the swift-evolution
mailing list