Special syntax for blocks that capture all variables as weak

Introduction

Introducing a special syntax for blocks who capture all variables as weak. For example:

publisher.sink {? self.update(button) }

Will be equivalent to

publisher.sink { [weak self, weak button] in 
    guard let self = self else { return }
    guard let button = button else { return }
    self.update(button)
}

Motivation

Many of the UI related use of blocks requires the developer to add lots of boilerplate code to allow weak capturing of self and other variables used in the scope of the block when updating the UI to avoid retain cycles. It also doesn't make sense to strongly capture the UI only for updating it if its already dismissed.

By introducing the new syntax to weakly capture all variables in the scope we can also avoid bugs caused by the default behaviour of implicitly strong capturing all variables in the block scope.

This moves the focus away from deciding if strongly capturing a variable in this block will cause a retain cycle or not to letting the developer focus on whether this block must execute or is the block optional and should only be executed if all of its non optional capture list still exists.

Effect on ABI stability

The proposed solution maintains ABI stability since it introduces a new syntax that doesn't effect previous swift versions.

Discussion Summary

  • @Morten_Bek_Ditlevsen: how would optional chaining in the synthesized ‘guard let’ work?
    @eventomer: Guard statements will be added only for variables that were not optional when they were captured

  • @Morten_Bek_Ditlevsen: Could you write capture lists in this syntax? For instance if you actually need a single strong capture?
    @eventomer: I can think of reasons for both ways. lets hear more input regarding this

  • @Morten_Bek_Ditlevsen: Should a similar {! … } exist for capturing everything unowned?
    @eventomer: it does feel natural to have support for {! … } as well however personally i didn't use unowned yet in any context so i'm not sure what will be the value of having it but i'm open to it too if anyone else finds value in it

6 Likes

Could you write capture lists in this syntax? For instance if you actually need a single strong capture?

Another question regarding optionals. If ‘button’ in your example was optional would the closure only be run if the button exists?
In other words: how would optional chaining in the synthesized ‘guard let’ work?

Good point! I think it only makes sense to guard against non optional types captured variables.

I can think of reasons for both ways. lets hear more input regarding this. what is your take on it?

I don’t think I have an opinion (yet) - I’m just brain storming. :slight_smile:

I don’t know yet if I’m a fan of introducing this new syntax although I have been using weak self + guard let unwrapping a long time in the context of RxSwift observables subscribed to from the ui…

Should a similar {! … } exist for capturing everything unowned?

it does feel natural to have support for {! … } as well however personally i didn't use unowned yet in any context so i'm not sure what will be the value of having it but i'm open to it too if anyone else finds value in it

1 Like

The proposed spelling is conflating optionality (?) with weak references (weak). Just because you make a strong reference optional doesn’t mean it becomes a weak reference. You need to explicitly tell the compiler – and readers of your code – that it’s a weak reference.

If the language did include this feature (and I’m skeptical that it should) it would need to be spelled in a way that makes it clear that the captures are weak, probably using the word weak somehow. That the captures are optional would then follow from that – not the other way around.

8 Likes

What about just [weak] as the capture list? I.e. if you don’t specify any values to capture, you implicitly capture them all.

13 Likes

We need to convey both the weak references and the guards that bail out whenever a single one of the weak captures has turned nil. We're after a combo.

I pretty much like the proposal. It does only one thing, very useful, without much fuss. For all other cases, we'd have to use the existing capture syntax, which is pretty much ok.

3 Likes

This topic has been discussed periodically over the years. Here are links to a couple threads on the topic: Guarded closures and [Draft] Guarded closures and `@guarded` arguments.

1 Like

You're right. But this pitch, which deals with the specific case "every references are weak and all the block implicitly returns nil if any one is nil", with a single sigil, is very new, and very simple.

All critics so far against implicit weak references turn around:

  1. one usually should use strong references
  2. or at least use unowned references

(1) ignores the occasions when we don't care about the side effects (typically UI code)

(2) ignores the fact that unowned references require a great deal of trust that the code won't run after captured values have been deinited. This trust is sometimes impossible to grant, especially in racy contexts. For example, I do not trust Combine. And I do not trust the async tooling we'll soon profit from, due to its cooperative cancellation handling.

I really think this pitch is new, useful, and simple.

1 Like

I prefer this.

This would be fantastic.

If the ? syntax is not supported by others, even something like this would be better than doing the weak guard dance at the moment:

publisher.sink { [autoWeakGuard] in 
    self.update(button)
}

As someone very new to swift, I like this spelling much better. It’s clear, helps with discoverability/learning, and seems to provide a standard look to possible future ways to set/change the defaults to automate boilerplate code.

Maybe we should go at it with a different approach. Instead of a blocks syntax change we can use attributes the same way we use the @escaping attribute.

So if we have a block which we know that we are going to keep as a strong reference we can add @weakCapturing and @guardCapturing attributes to indicate how its capture list should behave.

For example I would be able to declare the following function:

func addAction(_ handler: @escaping @weakCapturing @guardCapturing (UIAction) -> Void)

@weakCapturing indicates that the default capturing behavior is to weak capture all variables in the capture list

@guardCapturing indicates that the block should only be called if all the non optional variables in its capture list exists

(thanks @German.Velibekov for the inspiration)

1 Like

How would that work with functions that aren't your own? (could it?)

I was reworking a observed node graph today. Not having time to rewrite everything, i wrapped the handlers in captures to add unowned self. (It was nice getting hard crash to see where my deductions of ownership direction were wrong)

This syntax would have allowed me to change just the definitions and have the ownership or guard be automatic.

Almost always you don't need a weak capture and unowned is safe to use:

publisher.sink { [unowned self] in self.update(self.button) }
  .store(in: self.disposeBag)
1 Like