`guard` capture specifier for closure capture lists

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

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