`guard` capture specifier for closure capture lists

If one uses escaping closures mainly as an alternative to delegates in UI code (as event handlers) then the majority of the closures require weak captures to break reference cycles. And having guard let self = self else { return } statements all over the place is quite distracting.

2 Likes

Weak captures everywhere are definitely an anti-pattern, definitely over-used, and this would likely make it worse. A lot of developers don't really understand this issue beyond: "retain cycle bad", and "weak means no retain cycle". They use it everywhere.

So then we get to this idea. It's one of those things that people think they want, because they feel it would be nice for us to do something to make retain cycles easier to manage; but if you gave it to them, they wouldn't be satisfied. Because it doesn't really do anything about retain cycles - you still have to identify where they might happen, still add capture lists, etc. It requires just as much effort and understanding and awkward syntax as it always did -- only now, sometimes your closures will magically not run.

If you think retain cycles are a problem, think bigger than this. If this "sugar" would solve your problems, your problems aren't that big.

7 Likes

This gets to the heart of it doesn’t it. Implicit return early vs explicit return early. I think explicit return early is better for readability.

2 Likes

+1 from me. It's not a perfect solution, but this is a longstanding problem and I see no evidence of a better solution on the horizon after several years of waiting.

I'm also fine with guard as the keyword since it's evocative of the pattern that it's replacing. if as a modifier seems weirder to me.

4 Likes

I think I like it.

The limitation to void isn’t ideal. I’d prefer it to work for Void or Optional return types (returning nil). Something even more flexible would be nice but I can’t think of good syntax for providing a default value so Void/Optional will need to be enough.

What is the behaviour where an optional value is captured? Does it become a double optional as weak and then the guard only unwraps the outer value? This kind of issue I think highlights (against my initial gut reaction) that weak probably does need to be part of the declaration.

Maybe [weak(guard) foo]?

For UI actions this will be a very useful addition whether in the pitched form or a more extended form. I think for people not writing large amounts of UI code this may seem insufficiently important but that is a large use case and I think makes this worthwhile.

1 Like

I've kept the initial pitch focused on Void closures, since it is more "obviously correct" to just return in a Void closure, and Void closures seem dramatically more common in this type of action handling / event notifications paradigm I've been considering.

I do think it's worth discussing if we should support this for other types. I'd be curious to hear what other folks think about this. There isn't really a technical reason why we couldn't support Optional -- can anyone think of examples for/against supporting it?

One approach I've considered is supporting any type that is ExpressibleByNilLiteral (so the guard would just return nil). This includes Optional, of course, and would open the door to supporting user-defined types as well.

Testing this in a playground, I see that a weakly capture of an optional variable is a single-optional. I suppose this is because you can't "weakly" capture an Optional itself (which is a value type), only the wrapped value (which may be a class).

1 Like

As a general design principle, Swift declines to have implicit default initialization. That is, Optional is never implicitly initialized to nil, Int is never implicitly initialized to 0, etc. There is no limiting principle here that stops at Optional and not, say, Result and so forth, and in essence this would upend an explicit design decision of the language.

2 Likes

how come? var x: Int? makes x nil implicitly.

in regards to the pitch. -1 for how it handles return values, there must be a way to return an explicit value. +1 for how it allows implicit weak self.

1 Like

Ah yes, that one wrinkle. If I recall, the ones responsible for that decision have discussed on these forums that they would not make that choice for Swift if they could do it again, but it is probably too late now to take it out.

4 Likes

i see. i actually like that wrinkle. except here, where it "feels" absolutely wrong:

struct S {
    var a: Int
    var b: Int = 0
    var c: Int?
    var d: Int? = nil
    var e: Optional<Int>
    var f: Optional<Int> = nil
}

S(a: 0, e: nil) // didn't have to specify "c" ?!
2 Likes

as an alternative to consider, we may allow implicit self after guard let self = self else { return ... } dance:

{ [weak self] in
	self?.x = 1
	guard let self = self else { return xxx }
	self.x = 1
	x = 1 // ALTERNATIVE (not currently possible)
}

I like the proposal. Clearly communicates the intent and guard as a name has IMHO the desired symmetry to a guard statement.

1 Like

Yes, good point in illustrating that the exception applies to Int? specifically and not to Optional<Int>. That is, the implicit = nil comes with the shorthand notation and does not change the fact that Optional isn't implicitly initialized to nil.

3 Likes
Indeed

if we decide this particular aspect is "more bug than a feature" it's probably very easy to fix - just treat var c: Int? as if it was written as var c: Optional<Int> when it comes to auto generated default initializer. but that'd be a source breaking change, so needs to go via "warning" -> "deprecation" -> "error" stages over a few years.

1 Like

I think this is an important point that probably deserves more attention. What if self being nil is actually indicative of a problem which is being silently ignored? weak self has legitimate uses, but it can also be used thoughtlessly, and it seems like this proposal could make it easier to do so.

5 Likes

Good question. I agree that [guard self] will not be applicable to all [weak self] closures that return Void. Folks will need to reason through the implications of "this closure will not run if self no longer exists".


I think it's worth considering what overall portion of weak self closures fall into the category of "it is clearly correct for the closure to not run if self no longer exists".

I took a brief survey of [weak self] closures in the Airbnb codebase (2M+ lines of Swift code):

  • ~9,500 uses of weak self
  • ~2,000 weak self closures that start with guard let self = self else { return } (30%)
  • ~4,500 weak self closures that use self?. in the first line (which, for Void closures, implicitly does not handle the case where self is nil) (47%)

These two cases (that I could trivially regex for) cover 70% of all weak self closures in our codebase. It seems likely that this could cover an even larger percentage with more sophisticated regexes (that handle multi-line guard cases, for example.)

Within the context of a UI codebase, it seems reasonable to make the claim that the proposed semantics of [guard self] cover the large majority of existing use cases of [weak self].

3 Likes

Absolutely correctness is the biggest concern here. However, unowned self is a legitimate optimization in many cases that also makes an assertion about the object graph. How many of those [weak self] could be upgraded to [unowned self]?

2 Likes

There are valid reasons to prefer [weak self] with a guard over [unowned self]. Many folks consider unowned self as "risky" as force-unwrapping optionals.

This is why we have ~9,800 weak self closures in our codebase, but only ~250 unowned self closures.

5 Likes

So, I want to make a non-technical case for weak self. I've made a similar post in the past, but want to reiterate it here. For me, the largest difference between weak and unowned is the fact that when an assumption about a weak reference is broken, it's handled via a low-priority bug report that gets fixed in the next release. A broken assumption about an unowned reference results in a P0 crasher that has to have everything dropped in order to put out a hot fix. It might be more likely to catch unowned issues before shipping, but I'd take many weak bugs over one unowned bug, purely from a quality of life perspective.

Even if I can fully plan out the ownership graph and be certain that unowned is appropriate in a particular case, such a decision isn't robust against future changes and refactorings that may have non-local implications.

8 Likes

Thanks, both. I think those two posts capture the tradeoffs pretty well: with weak the consequence of your assumptions being wrong is that an action doesn't happen (occasionally catastrophic for users if it results in data loss, but usually benign; hard to debug); with unowned the consequence is that the app crashes (pretty much always catastrophic for users; easy to debug). "Easy to debug" is extremely valuable if the situation comes up during your testing, but may not be worth the "catastrophic for users" when it doesn't—and object graph issues are, for better or worse, hard to test.

Still, as someone who believes force-unwrapping is often the right tool for the job, a policy of "default to weak" definitely rubs me the wrong way, and adding this sugar encourages that.

Avoiding crashes with unowned requires non-local reasoning. It is impossible to know whether or not the captued objects will still exist when the closure is called without reading all code that can potentially call the closure.

Closures aren't called arbitrarily. They're event handlers, or pipeline transformers, or tasks-on-queues. You should have an idea of the lifetime of a closure just like you have an idea of the lifetime of a framework object. (And none of this discussion applies to non-escaping closures, which can always assume self exists. If you're using [weak self] with a non-escaping closure, you can freely delete it.)

8 Likes