[swift-evolution] [Pitch] Introduction of `weak/unowned` closures

Paul Cantrell cantrell at pobox.com
Sat Jun 10 16:42:40 CDT 2017


Being able to specify things about closure capturing at the API level could do wonders for Siesta — though its problems may be beyond the scope of what’s proposed here (or what’s workable at all).

Siesta exposes this API call:

    someResource.addObserver(owner: self) {  // (1)
        [weak self] resource, event in
        …
        self.bar()
        …
    }

…but it’s easy to forget that [weak self], and because of Siesta’s observer ownership rules:

    http://bustoutsolutions.github.io/siesta/guide/memory/ <http://bustoutsolutions.github.io/siesta/guide/memory/>

…omitting it creates a retain cycle. This same problem also makes this otherwise lovely pattern untenable:

    someResource.addObserver(owner: self, closure: self.fooUpdated) // (2)

To solve this problem, addObserver would need to be able to specify that the object passed as the owner should be weakly captured by the closure. It’s just that one specific •owner• object; everything else should be captured as usual. So it’s not a property of the closure itself, either in the type system or as an argument attribute; it’s a relationship between the closure and other args. Ouch!

At a glance, it looks like Matthew’s guarded closure proposal might solve this in practice for (2) but not for (1)?

In the absence of cycle detection, some way around this pitfall sure would be nice.

Cheers, P


> On Jun 10, 2017, at 12:49 PM, Gor Gyolchanyan via swift-evolution <swift-evolution at swift.org> wrote:
> 
> The benefit that I can see here is the ability to guarantee memory safety on API level, by way of specifying weak closure members. Previously, there way no conceivable way of knowing that because a closure can essentially capture whatever it wants (even the very object it's stored in), so this would be a deterministic way of resolving *all* circular references caused by closures.
> 
> The downside is that it would require some heavy-duty closure capture analysis on the compiler's part, so I'd expect it to be deferred to Swift 5 or something.
> However, this does play really nicely with the ownership concept that Swift is going for.
> 
> I say, let's think this one through very thoroughly and write a very very detailed proposal (including details on how would the core team get around implementing this) and see what it looks like before submitting it.
> 
>> On Jun 10, 2017, at 8:29 PM, Adrian Zubarev via swift-evolution <swift-evolution at swift.org <mailto:swift-evolution at swift.org>> wrote:
>> 
>> Hello Evolution,
>> 
>> I’d like to pitch a new idea and see where it would go. Recently I tapped into a small trap and just now realized that even that non-escaping should have been the default for closures (SE–0103) there is an exception for that. Apparently generics don’t follow that rule and a closure like 
>> 
>> Optional<() -> Void> or simply (() -> Void)?
>> 
>> is still escaping by default. But that was the half of the story yet. As we all know and “love” reference lists inside closures, methods don’t have any and we have to wrap method calls into a weak referenced closure 
>> 
>> { [weak self] in self.foo() }
>> 
>> to avoid strong reference cycles. Maybe you already guess it, I accidentally didn’t and tapped into the land of strong reference cycles yet again on my journey.
>> 
>> I’d like to pitch a new way, more like a new type behavior, for closures on how they could be used differently in order to avoid strong reference cycles but also providing the ability to use methods without any need to wrap them.
>> 
>> Here is a simple code snippet using RxSwift, which will recreate my issue:
>> 
>> import RxSwift
>> 
>> let test = PublishSubject<Void>()
>> 
>> class A {
>> 
>>     let disposeBag = DisposeBag()
>> 
>>     func foo() {
>>         test.asObservable()
>>             .subscribe(onNext: self.bar) // The issue is here
>>             .disposed(by: self.disposeBag)
>>     }
>> 
>>     func bar() { print("works") }
>> }
>> 
>> let a = A()
>> a.foo()
>> 
>> test.onNext(()) // Testing if it works
>> test.onCompleted() // Some RxSwift stuff
>> In this case by passing directly the method self.bar we’re capturing self, which in this situation isn’t our intention at all. To avoid this issue we can simply wrap the method call into closure:
>> 
>> .subscribe(onNext: { [unowned self] in self.bar() })
>> 
>> (It’s safe to make it unowned because the dispose bag is a member of self.)
>> 
>> What if we had the ability for weak or unowned closures? By that I don’t mean weak/unowned references to the closures themselves, because they are also reference types, but an invalidation behavior for the whole closure based on the _captured_ references. For instance:
>> 
>> let closure1: weak (() -> Void)? = { self.doWhatever() }
>> 
>> let closure2: weak (() -> Void)? = self.doWhatever
>> 
>> If one would now try to call the closure, first it will check if all the captured objects are still available or not, if not the whole closure in this case will simply become nil and won’t execute. In case of unowned closures it will trap. Furthermore it will support the general meaning of weak/unowned and will not increase the reference counter for *captured objects*.
>> 
>> As you have already noticed, in this case the convention is slightly different because we must carry the behavior directly with the type.
>> 
>> func subscribe(onNext: weak ((Swift.E) -> Void)?)
>> 
>> If the way of my thinking is correct this idea _could maybe_ fade out the very common [weak self] in guard let strongSelf = self … pattern. 
>> 
>> I personally cannot tell all the technical difficulties this idea might have, but that’s what the evolution list is for, to collaboratively flesh out the ideas if they are worth it.
>> 
>> If something like this could be possible it’s probably worth noting that we might also be able to introduce something like @autoclosure(weak/unowned) to Swift for consistency.
>> 
>> 
>> 
>> 
>> -- 
>> Adrian Zubarev
>> Sent with Airmail
>> 
>> _______________________________________________
>> swift-evolution mailing list
>> swift-evolution at swift.org <mailto:swift-evolution at swift.org>
>> https://lists.swift.org/mailman/listinfo/swift-evolution <https://lists.swift.org/mailman/listinfo/swift-evolution>
> 
> _______________________________________________
> swift-evolution mailing list
> swift-evolution at swift.org
> https://lists.swift.org/mailman/listinfo/swift-evolution

-------------- next part --------------
An HTML attachment was scrubbed...
URL: <https://lists.swift.org/pipermail/swift-evolution/attachments/20170610/bc6ab2d7/attachment.html>


More information about the swift-evolution mailing list