`guard` capture specifier for closure capture lists

We already have precedence with optional pattern matching/unwrapping, e.g. case let myOptional?. Using the same with capture lists, e.g. [weak self?, weak button?] is consistent with how ? is used in that final position, while retaining the meaning of weak/unowned. The same could apply to if let myOptional?. Expressive and clear.

How about an [assert self] that would halt in debug but do nothing in production.

That would combine the safety of weak with most of the correctness assurance of unowned.

[Edit: I should have read to the end before replying, I see others have got to more or less this proposal first. I probably prefer asserting to the guard although I wouldn’t mind the existence of both. Regarding other crashes in my projects I tend to avoid force unwrapping too and we have added array subscipts and nil coalescing operators that assert rather than crash.]

1 Like

I've come around to the idea that including an assertionFailure is the best (safest and most correct) behavior for this new capture specifier.

Is there a nice spelling for this behavior that also includes the weak keyword? Like @jrose has mentioned, I think including weak would be helpful for clarity here. Following the precedent of unowned(safe) and unowned(unsafe) using parenthesis may be desirable as well.

That could potentially give us [weak(assert) self] or [weak(guard) self].

  • [weak(assert) self] makes it clear that an assertionFailure may be emitted, but doesn't really indicate the presence of an additional control flow condition.
  • [weak(guard) self] makes it more clear that there is an additional control flow condition, but may obscure the fact that an assertionFailure may be emitted.

Between those two options, I'm sort of leaning towards [weak(guard) self]:

  • Explicit control flow is probably the most important consideration, in my opinion. I think the final spelling needs to be as clear as possible that the closure body will only execute if self still exists. Reusing an existing control flow keyword (guard) seems like an effective way to do this.
  • In comparison, it seems relatively less important to emphasize the presence of the assertionFailure. If the assertion is hit, then it would be impossible to ignore (since it would halt execution). If the assertion is never hit, then there's not really any harm in it being implicit.
2 Likes

A "middleground solution" could be to make the weak self checks less verbose.
For me having a shorthand for doing the weak self checking would already be an improvement and imo also useful in other contexts.

let closure = { [weak self] in
    guard self // only works for Void returning closures
    …

}
// is equivalent to 
let closure = { [weak self] in
    guard let self = self else { return }
    …

}
1 Like

Yikes. I really dislike both of these.

While I'm not in favor of the proposal generally, its syntax is much more immediately understandable than the mess that these two present.

I'm sorry to shoot you down, I appreciate the thought and work you've put in, but while the motivation is well-placed (eliminating some common boilerplate), but I strongly believe we should not accept this proposed change.

  • Weak captures where you would not want the closure executed if they are nil, while common, do not come close to the 80% use mark (i.e., too narrow of a use case). Weak captures in general aren't even the most common case.
  • The syntax [guard self] conflates guard and if, having the early-return behavior of guard but the "only proceed if true" behavior of if. This is confusing.
  • Only works with closures returning Void. Again, much too narrow a use case.
  • Doesn't allow for easy changes in the code. If I have [guard self], [guard button] and then decide I still want to execute the closure if button is nil, but not if button.text is nil, I can't use [guard self], [guard button?.text], so I instead now have to write:
{ [guard self] in 
    guard let text = button.text else {
        return
    }
    //...
}
  • And what about when I want to debug why the closure didn't execute? How will I set the breakpoint? I'll do it by re-adding that boilerplate code.

There will be this constant dance of adding that boilerplate -- either to log something, or for debugging, or because I've decided my closure shouldn't return Void (or that it now does), or really for any slightly more complex use case -- that will make this addition cause more pain than any helpfulness it may bring.

Using [require self] removes the if/guard conflation issue, but the rest of the issues stand.

4 Likes

I guess I disagree on this bit as well lol.

I really think this is more of a solution in search of a problem. The longstanding problem isn't really a "problem". The use case is common but is far from the 80% mark. Accepting this flawed solution because we haven't seen better ones yet is just adding edge cases and corners to the language.

3 Likes

I acknowledge that this may not be the case in the code that you write, since folks have different preferences.

It's not trivial to get this sort of statistic for "all Swift code" in general, but you can use a GitHub search for "weak self" in Swift code as a reasonable proxy. Since GitHub doesn't support regex search, I took a random sample of 50 of these results (raw results are here). In summary:

  • 15 (30%) used guard let self = self else { return } and did not handle the case where self is nil
  • 13 (26%) used an if let self = self { ... } that wrapped the rest of the closure body, and did not handle the case where self is nil.
  • 17 (34%) used self?. on all lines of code within the closure, and did not handle the case where self is nil.
  • 1 (2%) used guard let self = self else { ... } and actually handled the case where self was nil
  • 1 (2%) used self?. on some, but not all, lines of code within the closure (so does handle the case where self is nil, perhaps unintentionally).
  • The remaining 3 results were not actual examples of [weak self] closures (GitHub search does not support exact search).

By these numbers, 90% or more of weak self closures only execute if self is still present.


This is an important topic, that was not well addressed in the first pitch draft. @NathanLawrence suggested including an assertionFailure in the else branch, so execution would be halted in debug builds. This would prevent "silent failures" during development.

2 Likes

Hm, thanks for the clarification - your numbers are very interesting. I took a look in the codebase of one of my projects, searched for "weak self" and then looked at 35 of them and found 16 where the closure could not have been run at all if self were nil.

1 Like

Sorry, -1 on this. My feeling is we are trying to replace clear simple code doing a simple thing with more complex code doing something less fathomable and more error prone.

5 Likes

I still think that the natural sugar for this is to extend case let foo? to if/guard statements.

let closure = { [weak self] in
    guard let self? else { // works everywhere as long as you name an existing optional.
        …
    }
    …

}

i don't mind much of where and what to put in the capture list or the guard line (e.g. "with self" ?) so long as i can use self implicitly within the block! this is not possible now.

check self somehow
    self.foo()
    self.bar()
    self.baz()
    vs
    foo()
    bar()
    baz()
}

the current "workaround" would be to use a local function.

1 Like

Being able to implicitly call methods on an unwrapped self would be nice, but I'd also like a general feature for that. Particularly, I'd like to be able to specify which value is implicit when you declare a closure, so that can be any parameter or capture and not just a self capture.

how about this:

with self, other {
    foo() // or .foo()
    bar() // or .bar()
}

instead of this:

if let self = self, let other = other {
    self.foo()
    other.bar()
}

i appreciate we do not have "with" now, and that its usage in other languages has nothing to do with optionality checks. if we want to have a difference between the normal usage and optionality check to stress the point the block is executed conditionally the latter case could be expressed like so:

guard with self, other else { ... }
foo() // or .foo()
bar() // or .bar()

if with self, other {
    foo() // or .foo()
    bar() // or .bar()
}
1 Like

I actually had something in mind that would let us write our own with, an annotation for specifying an implicit parameter or capture.

func with<T>(_ initial: T, update: (@implicit T) throws -> ()) rethrows -> T {
    var value = initial
    try update(&value)
    return value
}

…
// The same as guard let self = self, let other = other
guard let self?, let other? else { return }
_ = with(self) {
    foo() // or $0.foo()
    other.bar()
}

// as a capture
{ [@implicit unowned self] in
    foo() // or self.foo()
}

I honestly don't think it's a good idea to allow multiple implicit parameters in single function.

By the way, for anyone interested — I updated the proposal to reflect the discussions in this thread and submitted a PR to the Swift Evolution repo: Add proposal for `weak(guard)` closure capture specifier by calda · Pull Request #1429 · apple/swift-evolution · GitHub

2 Likes

This is pretty narrow, and I don't love that the control flow is both implicit and hard-coded. I like @tera's suggestion to just add a very narrow "guard rebind":

{ [weak self, weak target] in
  guard self?, target? else { return } // sugar for "guard let self = self, let target = target"
  target.action(on: self) // self and target are non-optional here, and we would allow implicit use of non-optional self
}
13 Likes

The core team talked about this, and we're reluctant to add features that introduce new sources of control flow unless they significantly improve expressive power. This proposal is a simple sugaring of a very specific pattern, but it would not work in a lot of superficially similar situations, like when the function needs to return anything other than Void. The amount of convenience offered also seems fairly low.

@cal, thank you for your suggestion, it was certainly worth considering. However, at this time, we don't think this proposal is likely to be accepted, and so it's not worth taking through the Evolution process. We can reconsider this if someone makes a strong argument to do so in the future.

13 Likes

Thank you for the feedback, I really appreciate the transparency!

Would the core team consider permitting implicit self access in weak self closures once self has been unwrapped? Like in your example above:

{ [weak self] in
  guard let self = self else { return }

  // implicit use of non-optional self is allowed, since self has been unwrapped.
  // e.g. this is equivalent `self.dismiss()`:
  dismiss()
}

If that seems worth evaluating on its own, I can come back with an implementation / standalone proposal for that.

8 Likes

Yeah, that seems like a problem that's well worth addressing.

6 Likes