`guard` capture specifier for closure capture lists

Hey folks, this pitch discusses introducing a new guard capture specifier for closure capture lists. This topic has come up before over the years ([1] [2] [3] [4] [5]), but has never been implemented before as far as I can tell (until now!).

This proposal is implemented in apple/swift#38718.

Introduction

guard captures behave like weak captures (e.g. guard captures do not retain the captured value), but the closure body is only executed if the captured objects still exist.

class ViewController {
    let button: Button

    func setup() {
        button.tapHandler = { [guard self, guard button] in
            if canBeDismissed(by: button) {
                dismiss()
            }
        }
    }
}

Motivation

In some classes of Swift programming, such as UI programming, closures are a predominant way to perform action handling and send event notifications between multiple objects. When passing closures between parent objects and their children, special care must be taken to avoid retain cycles.

weak captures are often the preferred method to avoid retain cycles. In action handling and event notification closures, there is typically no work to perform if self (or other captured objects) no longer exist. Because of this, a large number of these closures simply guard that the weakly captured object still exists, and otherwise do nothing.

For example, this sample code weakly captures self and button:

class ViewController {
    let button: Button

    func setup() {
        button.tapHandler = { [weak self, weak button] in
            guard let self = self, let button = button else { return }

            if self.canBeDismissed(by: button) {
                self.dismiss()
            }
        }
    }
}

This is a very common pattern, but has some notable drawbacks:

  • weak captures don't support implicit self.

    • As of SE-0269, strong and unowned captures of self enable implicit self calls within the body of escaping closures. This is not straightforward to support for weak closures in the general case, and was intentionally excluded from SE-0269.
  • guard let value = value else { return } is quite a bit of boilerplate in this context.

    • Closure syntax is lightweight, but the guard statement for a corresponding weak capture is relatively heavy.
    • In the example above the guard statement (which is effectively boilerplate) is more code than the rest of the closure combined.
  • unowned captures are not necessarily a suitable alternative.

    • unowned captures can also be used to prevent retain cycles, but will cause the application to crash if executed after the captured object has been deallocated.
    • Crashing on invalid access is memory-safe, but is often considered undesirable from the perspective of application stability.
    • 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.

Proposed solution

We should introduce a new guard capture specifier:

class ViewController {
    let button: Button

    func setup() {
        button.tapHandler = { [guard self, guard button] in
            if canBeDismissed(by: button) {
                dismiss()
            }
        }
    }
}

guard captures behave like weak captures (e.g. guard captures do not retain the captured object), but the closure body is only executed if the captured objects still exist.

When using [guard self], self is always non-nil in within the closure body. Following the precedent of SE-0269, implicit self will be permitted within closures that use [guard self].

Detailed design

guard captures desugar into a weak capture with a corresponding guard let value = value else { return } statement. This guarantees that the closure body, as written, will only be executed if the captured calue is non-nil.

button.tapHandler = { [guard self, guard button] in
    // ...
}

// is desugared into:
button.tapHandler = { [weak self, weak button] in
    guard let self = self, let button = button else { return }
    // ...
}

guard captures are only permitted in closures that return Void. Attempting to use guard captures in a closure that does not return Void will result in an error with the following fixit:

// 🛑 `guard` capture specifiers are only permitted in closures that return `Void`
// FIXIT: Change `guard` to `weak` and insert `guard let self = self else { return <#value#> }`
var canBeDismissed: () -> Bool = { [guard self] in  
    return true
}

Source compatibility

This proposal is purely additive, and has no effect on source compatibility.

Effect on ABI stability

This proposal desugars into existing syntax, so has no effect on ABI stability.

Effect on API resilience

After being parsed, guard captures are treated as weak closures. This proposal has no visible effects on the public API of closures. Changing a weak capture to a guard capture, and vice versa, is always permitted and has no effects on public API.

Alternatives considered

Support closures that return a value

In this proposal, guard captures are only permitted in closures that return Void. To support guard closures for closures that return an actual value, we would have to pursue directions like:

  • provide a default value that is returned if the captured objects no longer exist (e.g. return nil for closures that return an Optional).
  • provide a way to specify the return value in the capture list (like [guard self else nil], as a strawman syntax).

These sorts of designs either promote hidden behaviors, or would require new syntax that is not meaningfully better than the status quo.

30 Likes

While there is clearly appetite for something in this area, I should point out that the numerous discussions back in the early days of Swift Evolution took place when it was not even possible to write guard let self = self else { return }: allowing that syntax was itself a large step forward in ergonomics. Related also to this discussion are the numerous and recurrent discussions regarding abbreviating optional unwrapping.

My major concern with this proposed design, as with other proposals surrounding guard, stems from the observation that the major innovation and boon to correctness that comes with guard isn't what comes after that keyword but rather what comes after else. While there exist if statements without else, there is no such thing as a standalone guard statement: it's always guard...else. The else block is not only guaranteed to exit from the outer scope, but it must spell out exactly how that's to be accomplished, which is of-a-kind with how Swift's design requires users to reckon with how to deal with null values or thrown errors explicitly. Yet, invariably, proposals to provide further syntactic convenience in this area focus on eliding else and what comes afterwards.

Some time ago, Ben Cohen (I believe--sorry if it was another person) articulated several common criteria for standard library "helpers" that are sugar for composing existing APIs; doubtless the addition of new syntax would require an even higher bar, but the criteria are instructive:

  1. Is it truly a frequent operation?
  2. Is the helper more readable? Is the composed equivalent obvious at a glance?
  3. Does the helper have the flexibility to cover all common cases?
  4. Is there a correctness trap with the composed equivalent? Is there a correctness trap with the helper?
  5. Is there a performance trap with the composed equivalent? Or with the helper?
  6. Does the helper actually encourage misuse?

While you've clearly identified a frequent operation (at least with current Apple platform UI programming paradigms), in eliding the else it provides less (no) flexibility (criterion 3). The fully written-out version with explicit else { return } makes clear that the rest of the closure may not execute, which is lost with the proposed sugar, raising concerns for correctness/misuse as users are less likely to reasonable about an implicit branch than an explicit one (criteria 4 and 6).


On reflection, for me, a lot of these concerns would be addressed if this proposed feature were simply spelled as [if self], conceptually desugaring to an if let self = self { ... } that surrounds all the statements inside the closure. By contrast to guard, if doesn't imply a mandatory else, so conceptually there wouldn't be an elided, implicit branch here that users aren't reasoning about. Everything else about your design (including that it wouldn't be usable if the closure has to return a value) would fall out naturally from the fact that there's only an if branch and no hidden else branch. WDYT?

26 Likes

Interesting idea. I'm totally open to other spellings of a feature like this.

Suggestions I've seen so far include:

  • [guard self]
  • [require self]
  • [if self]

The ergonomics and semantics of all of these spellings would be the same, so we could choose whichever spelling results in the simplest mental model.

2 Likes

Right, I did not set out to make this a bikeshedding exercise about the spelling. I'm of course sympathetic to the need that's identified, but I started out writing my feedback with deep misgivings about the underlying semantics because of that elided else block, then realized that it all melts away (at least for me) if the underlying mental model is that it's a shorthand for if unwrapping and not guard unwrapping.

10 Likes

This looks great to me! It would remove so much boilerplate code.

1 Like

This is very sweet, it is just the right level of syntax sugar that makes things feel approachable. It would be quite nice for combine and a number of other APIs. Worth exploring more imho.

3 Likes

Does this implicitly assume the said closure is of return type Void? I’d imagine it might impossible for the compiler to generate a guard statement otherwise. I’d also prefer if the semantics of an early return is more obvious in the syntax!

1 Like

This looks like a sugary bit of goodness, and I would use it in my own work.

I am super hesitant about starting any further bike-shedding, but I agree with @xwu that guard isn't the right name for this, and also agree that [if self] is a better description of what's happening here.

4 Likes

This syntax would only be permitted in closures that return Void:

I like the model of thinking of this as an if rather than a guard, especially since it effectively defines away the question about what to do in the else branch.

I'm less sure how I feel about using if as the keyword for this. For example, [if self, if button] feels off in a way I can't quite describe (perhaps because it overloads such a common keyword). Maybe this is would feel natural after using it in practice.

Several folks on Twitter have suggested [require self]. I think I like require the most, possibly because it doesn't overload an existing keyword. It's very clear about the semantics (like [if self]), but doesn't imply the existence of a omitted else branch (like [guard self]). A "required capture" also seems like better terminology than an "if capture".

Thoughts?

2 Likes

The only concern I would have about that is that require has been unofficially used in Swift Vapor/Fluent a bit to do the same thing, but it throws if the required entity doesn't exist. I'm not involved enough in that community right now to know if that's going to be a problem.

That aside, the term in this use seems semantically correct.

1 Like

Could the requirement for the return type to be Void be extended so that it worked with Optionals as well, returning nil in the cause where any of the required capture parameters is nil.

I don't like this feature because I think this is an anti-pattern, but I'll toss in another spelling: guard weak self, or some other modifier on weak self. I think having the keyword weak is valuable for making the object graph ownership semantics clear; it makes it less "new syntax you have to learn the desugaring of".

20 Likes

I do like the idea of including weak, to make the memory/ownership semantics as clear as possible.

If we want to include the weak keyword, some potential spellings include:

  • [guard weak self]: seems reasonable. It's clear how this desugars.
  • [require weak self]: seems a bit less nice, may read like "require that self is weak".
  • [if weak self]: may read like "if self is weak", but its probably clear how this desugars.
  • [if let weak self]: seems reasonable. It's clear how this desugars. Potentially too much of a keyword soup.

Well, I'm a strong advocate (like a broken record) for the principle that similar things should look similar, and different things should look different. If we agree that the best model here is desugaring to an if statement, then I think the most suitable spelling for this is if.

I'm not sure that there is any organically occurring meaning a priori (i.e., besides what we would declare it to mean here by fiat) of "required" in this context; it overloads a totally unrelated term used for class instance methods but gives no hint of the underlying if model.

Sure, I think if (let) weak self reads quite nicely!

1 Like

I'm afraid I'm going to be a bit of a wet blanket here, but I don't think this would be a good addition to the language—at least not right now—for a number of reasons.

Code is read far more often than it is written. This change certainly makes code easier and faster to write, but that isn't an end unto itself for Swift; the goal is that the resulting code is also easier to understand.

This change doesn't pass that test. When looking at a closure, the only thing determining whether the statements inside it execute at all under certain circumstances is potentially a single keyword in the capture list. And if the suggested alternative of if self or if weak self is chosen instead of guard self (I agree with @xwu's reasoning why guard is a poor choice here), then we're talking about two non-whitespace characters changing whether an entire closure body is executed. I worry that the difference is very easy to overlook and it's burying information that should be more explicit.

It also seems to be somewhat common that people overuse [weak self] when they don't actually need it. Rather than encourage users to consider the implications of the memory ownership they choose, I worry that this feature just lets them dig deeper into bad programming practices—they'll just reach for [guard self] by default, it becomes the new [weak self], and then no closures would have any explicit control flow acknowledging the possibility that self or whatever was captured could have been released.

How would this feature interact with debuggers? If you have an explicit guard let self = self else { ... } statement and you suspect your closure is getting called after self is released when you expect it to still be alive, you can set a breakpoint on it and inspect the state of your program. How would you provide a comparable debugging experience if the control flow is synthesized by the compiler?

Philosophically, I don't think it makes sense to introduce special syntax into capture lists that conflates control flow and memory ownership. To my knowledge, no other construct in Swift does this. The closest would be the standard if/guard let x = x conditional bindings, but those work for any optional value—not just weak captures—so it's about general value presence and not memory ownership, and they still make the control flow (if/guard) independently clear from the binding (let x = x).

Moreover, we have to ask whether it's healthy for Swift long-term to extend it with special cases for very narrow use cases. guard let self = self else { return } uses standard language features that apply in numerous other different situations. But this feature:

  • ...only applies to closures
  • ...which capture references weakly
  • ...and which return Void
  • ...and where the behavior you want is to skip execution entirely.

We can speak of the complexity of the language and the compiler, or we can speak of the complexity of the documentation and the cognitive load for learners of Swift to understand which situations this feature applies and doesn't apply. I think this feature would be an increase in both.

There's a thread from a few months ago about another related topic that comes up frequently: simplifying if let x = x syntax. There's a lot of discussion there about the challenges involved, and in that thread at least a couple members of the core team have said they feel that the idea of simplifying optional bindings is worth revisiting. IMO, it feels premature to try to remove the boilerplate for this specific case before looking at the boilerplate for optional bindings in general. It's possible that a solution to the overall problem could reduce the boilerplate for this case such that it is less objectionable, and without completely burying important control flow information. Focusing on weak captures first and then doing general optional bindings later could end up with two very different constructs for similar features, making the language incongruous and difficult to learn/understand.

34 Likes

I feel that this would be a better fit for a possible future @propertyWrapper-like syntax for capture lists. The current proposal addresses a very common, but ultimately niche pattern.

I also don't agree with supporting the if keyword in capture lists, which is semantically an argument list rather than something that should represent control flow.

I like the idea and I described my thoughts some time ago in very similar way.

In a nutshell this syntax was proposed at the time:

{ [weak(guard) self] in ... }
{ [unowned(guard) self] in ... }

I would only suggest to even use shorter syntax to avoid writing weak and unowned and instead ? and !

{ [guard self?] in ... }
{ [guard self!] in ... }
2 Likes

In general I like the idea of simplifying things and getting rid of boilerplate. When I first heard of this proposal on Twitter I thought this could be a nice way to get rid of some serious boilerplate code like guard let self = self else {} and it probably would do exactly that.
But what would we really get in return? Another way to write something which is widely overused anyways and now it’s even more convenient to use as it was before.

As far as the other proposed key word combinations go:

  • I agree [guard self] without any else clause feels wrong. It breaks a common pattern.
  • [if self] just looks wrong. Sorry for the subjectivity.
  • [if let (weak) self] in is just too much. Yes, this statement would probably hold all the information we’d need to give the captured instance its complete context but then we’re just a few characters away from { [weak self] in if let self = self {} } and we wouldn’t have won much.

I really don’t know how to make my mind up about this proposal. I see the benefits. And I don’t know if adding sugar to salty code is a good idea. :man_shrugging:t2:

2 Likes

I would generally be supportive of this type of change. I am a big advocate of the 80/20 rule, and if you can make something that gets used a lot simpler, as the pitch proposes, I am generally all for it.

Nonetheless, I can’t support this pitch. Why? Because I don’t think weak block arguments should be common, and thus don’t fit my 80/20 rule of thumb.

I hear the collective gasp, so let me explain my reasoning: I rarely use weak in this context, because I am usually passing blocks to queues, where retain cycles don’t usually play a role, or I have a clear owner of the block, often a view controller or such. In these cases, the correct choice is either a strong reference, or unowned.

Using weak in these situations is an indication your code has no clear ownerships or object lifetimes. Often weak is used ‘just in case’. IMO this is bad, because you are disguising bigger issues in your code, and your code is effectively stating to others “This block has no clear ownership. It can be called at any time, even after the objects it uses have been removed”.

There are rare cases which don’t fit the cases I outlined, but not many. True standalone, unowned blocks are rare. And where they do occur, the current weak label, with a guard, suffices. These truly ‘unowned’ blocks do not occur often enough to warrant specialized treatment — they don’t pass the 80/20 smell test.

4 Likes
Terms of Service

Privacy Policy

Cookie Policy