[swift-evolution] [Draft] Guarded closures and `@guarded` arguments
Matthew Johnson
matthew at anandabits.com
Sun Feb 19 16:57:24 CST 2017
I want to thank everyone who commented on the first draft of this proposal. I continue to believe the basic idea is a good one but there were a number of problems with the details of the design presented in that draft. They key insight that led to this new design is the idea of using a sigil at the usage site to establish different semantics than usual for guarded closures. Special thanks to those of you who focused my attention on the call site.
This new draft presents a much stronger, more general design that I hope addresses all of the concerns expressed about the previous draft.
# Guarded Closures and `@guarded` arguments
* Proposal: [SE-NNNN](NNNN-selfsafe.md)
* Authors: [Matthew Johnson](https://github.com/anandabits)
* Review Manager: TBD
* Status: **Awaiting review**
## Introduction
Some APIs have what we might call weak callback semantics. This design is common in many of Apple's newer callback APIs. These APIs greatly reduce the possibility of unintentional reference cycles / leaks by guaranteeing that they do not extend the lifetime of the observation target.
It is possible to implement APIs with this contract in Swift today, but the is don't feel very natural to either library implementers, or more importantly library users. It should be possible to design an API with this important semantic contract that feels natural to use (and implement) in Swift.
This proposal introduces guarded closures (prefixed with a `?` sigil). Closures that use this sigil default to capturing any references as `weak` and guard the invocation to be a no-op if any of the guarded references have been released before it is invoked. If no captured references have been invoked, they are upgraded to strong references for the duration of the invocation. This provides syntactic sugar for a very common pattern in Swift code (sometimes called the weak self / strong self dance).
It also introduces `@guarded` as an annotation that can be used on parameters of function type. Users are required to use a guarded closure when providing an argument for a `@guarded` parameter. This annotation is will be used in cases where it is *usually* (not always) wrong for the argument to extend the lifetime of objects that might be captured by the arguemnt it is given. This is a generalization of weak callback semantics that this proposal calls guarded callback semantics.
An early version of this proposal was discussed in the thread: [`@selfsafe`: a new way to avoid reference cycles](https://lists.swift.org/pipermail/swift-evolution/Week-of-Mon-20170213/032374.html)
## Motivation
Accidentally forgeting to use weak references is a common problem and can easily lead to reference cycles. Some APIs (such as many kinds of callbacks / notifications / observers) are best designed such that users cannot extend the lifetime of objects captured by the function argument they are given (unless the user explicitly goes out of their way to state their intent to do so).
For example, `UIControl.addTarget(_:action:for:) does not retain the target, thereby preventing users from making the mistake of using a closure with an accidental strong reference. We can do something similar in Swift:
```swift
// in the library:
func addTarget<T: AnyObject>(_ target: T, action: T -> Int -> Void) {
// store a weak reference to the target
// when the action is fired call ref.map{ action($0)(42) }
}
// in user code:
class C {
init() {
addTarget(self, action: C.takesInt)
}
func takesInt(_ i: Int) {}
}
```
Both the library and the caller have to deal with a lot of details and boilerplate that we would prefer to avoid. The natural design in Swift would be to simply take an action function. Unfortunately if we do that we run into a problem:
```swift
// in the library
func addAction(_ f: Int -> Void) {
// store a strong ref to f, which might include a strong ref to a captured self
// later when the action is fired call f(42)
}
// in user code
class C {
init() {
addAction(takesInt)
// oops! should have been: addAction{ [weak self] self?.takesInt($0) }
}
func takesInt(_ i: Int) {}
}
```
Here the syntax is much nicer, but unfortunately we have unintentionally extended the lifetime of `self`. The burden of ensuring `self` is not captured or is captured weakly falls on users of the library.
It would very nice if it were possible to design an API that has weak or guarded callback semantics while still acheiving the more concise and Swifty syntax.
## Proposed solution
This proposal introduces guarded closures and the `@guarded` annotation for arguments of function type.
### Guarded Closures
A guarded closure can be created in two ways, both of which use the `?` sigil. The `?` sigil was selected because of it's relationship to `Optional` and to optional chaining.
First, the guarded captures are weak references with `Optional` type.
Second, anything following a `?` in an optional chain may or may not be invoked. The same is true of a guarded closure: its contents may or may not be executed when it is called by code that holds a reference to it. If any of the guarded captures have been released it immediately becomes a no-op that returns `()` or `nil`.
#### Bound instance methods
A guarded closure may be created by prefixing a bound instance method reference with the `?` sigil:
```swift
let guarded = ?myObject.myMethod
// desugars to:
let guarded = { [weak myObject] in
guard let myObejct = myObject else {
return
}
myObject.myMethod()
}
```
#### Inline closures
A guarded closure may be created by prefixing an inline closure with the `?` sigil:
```swift
let guarded = ?{ flag in
flag ? someMethod() : someOtherMethod()
}
// desugars to:
let guarded = { [weak self] flag in
guard let strongSelf = self else {
return
}
flag ? strongSelf.someMethod() : strongSelf.someOtherMethod()
```
#### Multiple guarded captures
```swift
let viewController = getAViewControllerOwnedElsewhere()
let guarded = ?{ flag in
flag ? someMethod() : someOtherMethod()
viewController.doSomething()
}
// desugars to:
let guarded = { [weak self, weak viewController] flag in
guard let strongSelf = self, viewController = viewController else {
return
}
flag ? strongSelf.someMethod() : strongSelf.someOtherMethod()
viewController.doSomething()
```
#### Capture lists
As with any closure, a guarded closure may specify a capture list. Any captures specified in the capture list are not guarded and instead adopt the semantics specified by the capture list. This includes `weak` captures.
```swift
let guarded = ?{ [weak object, unowned otherObject, strong thirdObject] in
aMethodOnSelf()
object?.doSomething(with: otherObject, and: thirdObject)
}
// desugars to:
let guarded = ?{ [weak self, weak object, unowned otherObject, strong thirdObject] in
guard let strongSelf = self else {
return
}
strongSelf.aMethodOnSelf()
object?.doSomething(with: otherObject, and: thirdObject)
}
```
#### Self reference in escaping guarded closures
Guarded closures do not extend the lifetime of any objects unless a `strong` capture is specified in the capture list. Because this is the case users are not required to explicitly reference self using the `self.` prefix inside a guarded closure.
#### Return values
Guarded closures bail immediately when one of the guarded captures has been released. This proposal handles return values by requiring them to always return `Void` or `Optional`. If the inferred type of the closure is not `Optional` it is wrapped in `Optional`. `Optional` returning guarded closures return `nil` when any of the guarded captures has been released. `Void` returning guarded closures simply become a no-op after the first guarded capture has been released.
### `@guarded`
This proposal introduces the ability for arguments of function type to be annotated with the `@guarded` attribute. When `@guarded` is used the caller must supply a guarded or non-capturing closure as an argument. This ensures that users of the API think very carefully before providing an argument that captures a strong reference, and requires them to explictly state their intent when they wish to do so.
Because `@guarded` only makes sense in the context of an `@escaping` closure it implies `@escaping`. Users do not need to specify both annotations.
This allows a library to do something like the following (a simplified version of a hypothetical future method on `UIControl`):
```swift
class UIControl {
var action: () -> Void = {}
func setAction(_ action: @guarded () -> Void) {
self.action = action
}
func fireAction() {
action()
}
}
```
This API can be used as follows:
```swift
myControl.setAction(?myMethod) // great! no reference cycle.
let f = ?myMethod
// ok: compiler knows f is guarded
secondControl.setAction(f)
// otherObject should live as long as the closure might be invoked
myOtherControl.setAction ?{ [strong otherObject] in
// `self.doSomethingElse()` is not required.
// The compiler knows we cannot create a reference cycle here.
doSomethingElse()
otherObject.doSomething()
}
// ok: this is not a guarded closure but it only captures by value, no references are captured
let name = "Swift"
anotherControl.setAction {
print("hello \(name)")
}
// error: closure capturing a reference must be guarded
yetAnotherControl.setAction {
anObjectReference.method()
}
// error: strong capture not allowed without capture list
lastControl.setAction(myMethod)
// ok
lastControl.setAction { [strong self] in self.myMethod() }
```
#### Externally captured closures
The previous examples all show inline closures and method references. We also need to consider functions that might be received as an argument or stored in an instance, static, or global property.
It is obvious that a function should be allowed to forward a function when it is received as an argument annotated with `@guarded`. In all other cases directly forwarding a closure is not allowed. It can however be wrapped in a guarded closure pretty concisely:
```swift
func foo(_ f: @guarded () -> Void) {
// ok
otherFuncTakingGuardedClosure(f)
}
func foo(_ f: () -> Void) {
// error: `f` is not guarded
foo(f)
// ok: explicitly wrapped
foo ?{ [strong f] in f() }
}
```
## Detailed design
The primary detail that isn't discussed above is that the implementation should take advantage of the semantics of guarded closures where possible. For example, as soon as it identifies that any of the references captured with guarded semantics has been released the entire context should be released and only a no-op stub should remain as long as the closure is referenced.
One additional detail: it should be possible to pass a guarded closure to an API that does not require a guarded closure unless there is a strong implementation reason to prohibit this.
Apple API annotation
## Source compatibility
This is a purely additive change to the language.
However, if it is introduced it is likely that a number of libraries would adopt `@guarded` semantics. This would be a moderately complex breaking change for users of the library that would impact a lot of user code. Automated migration would theoretically be possible but extremely non-trivial and therefore likely not provided.
I haven't done a careful review, but there may be some APIs that are part of Swift.org which for which it might make sense to adopt guarded callback semantics.
## Effect on ABI stability
I am not an ABI expert so takes these comments with a big grain of salt (expert input is welcome!).
This is a purely additive change. However, it is possible that guarded closures might have different ABI in order to take advantage of early release of the context.
It is also possible that an Objective-C annotation could be provided that would allow Apple frameworks to adopt guarded callback semantics when used from Swift. I am not sure what impact this would have if it happened.
## Effect on API resilience
Migrating an API from `@escaping` to `@guarded` is a breaking change.
## Future directions
It would be very useful to expose an `isAlive` property on guarded closures that would allow libraries to detect whether the guarded references are all still alive or not. This would allow libraries to discard guarded closures which have become no-ops.
It might be interesting to allow users to specify a default return value that is used after one of the guarded references has been released. If this were possible we would not have to restrict guarded closures to `Void` and `Optional` return types.
## Alternatives considered
We could leave the situation as-is. This would be very unfortunate. Some APIs are better with weak or guarded capture semantics. It should be easier to design and use these APIs in Swift.
I considered various ways of exposing API on function objects. One possibility is to allow libraries to access the whole closure context or parts of it directly along with the unbound function. Another is to allow a library to derive a new closure with a modified self capture that has guarded semantics.
These solutions are more complex than necessary and unnecessarily expose implementation details.
Sent from my iPad
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <https://lists.swift.org/pipermail/swift-evolution/attachments/20170219/6157202d/attachment.html>
More information about the swift-evolution
mailing list