Pitch: Syntactic sugar for circumventing closure capture lists

Sorry if it is obvious, but why wouldn't we prefer to use self.foo(bar:)? It makes much more sense to me as we're referring to that specific function (in case there were other overloads). Specially since you seem to have a concerns for " proliferating the self.foo syntax" I would be happy to stop that proliferation.

I wonder if that should be a separate pitch from the other desired changes. It could be done without any changes to the current APIs, and would also apply if/when a new API is added. No idea if it would be easy to implement or not...would also be very much a breaking change so may not get approved.

Side note: I'm curious how often capture lists are used for anything other than self. I think the only time I've seen it is in examples.

Very rarely for me, but it does happen.

If the operations within my closure are restricted to one or two members on self, I'll forgo putting self in the capture list and put those members there instead.

It's cleaner than capturing self as now those members don't need to be prefixed with self so the code is just as terse as it would be in a non-escaping closure – with the exception of the capture list, of course.

This is exceedingly rare though, 95% of the time it's a [weak self] or an [unowned self].

And if i'm capturing self, very often it's one of these 'trampoline' closures that just forwards to a factored out local instance method.

1 Like

Huh...I hadn't thought of that. Good idea! I'm really hoping for a future version of Swift where we can have something like an @UnwrappingWeakSelf prefix where self isn't even required in the closure body and kill 2 birds with one stone.

Whoa, hang on. I think it's a jump to say [unowned self] is safer than [strong self], and [weak self] + guard + return definitely isn't safer because it leads to silently ignoring results (and silently ignoring failures).

I'm really curious what cases you all are using [weak self] with so much that you want sugar for it. [unowned self] I get because you want to avoid reference cycles, but [weak self] implies that self might go away before the callback finishes, and, like, shouldn't you know if that's going to happen?

EDIT: [strong self] has benefits for a default because it's consistent with the behavior for structs that contain references.

6 Likes

Again, animation completion handlers come to mind. The main body of the animation is fine with self unowned as it executes immediately, but the completion closure may occur after some user interaction has caused UIKit to discard the view before the animation completes and so you need a weakly captured self there afaik.

2 Likes

One example from a current project is AVPlayer.addPeriodicTimeObserver. And really, any time you're doing something in another thread. Network calls, disk saving operations, etc.

After 1 too many instances where I ended up with a dereferenced Object, now I generally tend use weak instead of unowned since AFAIK it's always safe. Though admittedly I may not understand the semantics completely.

2 Likes

Yeah, that would be welcomed by me, too – something to clean up all those self prefixes and guard let self = self else { return } calls!

1 Like

With animation completions it should be okay to strongly capture self, no? The completion closure has a short lifetime (slightly longer than that of the animation itself) and should be discarded immediately after it has been called. Why do you need weak self?

Personally, I've worked on codebases with rules (out of my own control) of the form "always capture self weakly to avoid the possibility of creating reference cycles." Not sure that questionable style guide decisions are good motivation for new features/sugar, though.

1 Like

I use it often in iOS coordinators when closures on vc need to access parent vc’s (like a navigation controller). Tricky to catch.

What situations are there where that would have a negative result? Assuming the performance is close enough to not be relevant.

It obviously depends on the case but 90% of async stuff happening in an app screen has this problem. The screen (self aka VC or VM or whatever) may go away at any point and operations may finish after. In some apis callbacks will still be called (for example giving you a cancellation callback) and thus weak self is the safer bet. In that case guard self is fine, silently ignoring is what you want.

2 Likes

Happy to be re-educated here, but here's my understanding:

Say we have a UIView with a UIViewPropertyAnimator:

class MyView: UIView {

   ...
   var propertyAnimator: UIViewPropertyAnimator?
   ...
}

At some point, due to some event, we want to animate some properties on that view and do some cleanup so we have:

class MyView: UIView {

    func performImpressiveAnimation() {
        let animator = UIViewPropertyAnimator(duration: 1, timingParameters: UISpringTimingParameters())
        animator.addAnimations {
            self.myFactoredOutAnimationInstanceMethod() // strongly held self, retain cycle
        }
        animator.addCompletion { _ in
            self.myPostAnimationCleanupInstanceMethod() // strongly held self, retain cycle
        }
        animator.startAnimation()
        self.propertyAnimator = animator // retain the property animator or no animations
    }

}

If we don't retain the property animator in this situation – the animations don't happen. So we make it an instance property on our UIView.

Therefore when we add the animation blocks and completion handler we have a retain cycle via: self -> animator -> animationBlock -> self.

We need a capture list for our closures.

For the addAnimations(_:)call we're OK with an unowned self – our instance always kicks off the animation so we can guarantee its safety. For the addCompletion(_:) call we need to use weakly as the view may be removed through some user/system event and deallocated prior to the animation block completing and an unowned self would yield a crash.

2 Likes

Unowned is not good default, agreed. That was my mistake.

I guess between strong and weak, it's question of balance. With strong you don't lose the reference and then you don't get crashes. With weak you might, especially if you force it, but usually you have the self?.foo, i.e. question marks to take care of it (and makes programmer knowledgeable about the issue). With weak you break most, if not all reference cycles, so you're not burning memory, whereas currently with strong it's given that your program will have one or more closure reference cycles unless you explicitly do the weak dance yourself (and you have to know about it).

At least for me, the default in closures is almost always that the (view) controller outlives the lifetime of the closure (e.g. a closure running on button click). So the weak reference to self will always be valid when it needs to, but also will not retain the closure in memory when controller goes away. so ARC does the right thing automatically, instead of me having to jump into manual memory management mode.

If you take a look at github repos and just to try find:

guard let strongSelf = self else { return }
guard let `self` = self else { return }`
...

you will get bazillion results.
The main problem is to understand when weak unowned should be used and even for experienced Swift users it is sometimes hard to grasp which one should be used as it strongly depends on the context. Thats why we have such situations were people add [weak self] regardless if is needed or not but it’s easier, and faster to add this than thinking about each case and for sure finding a memory leak which in case of closures is rarely pleasant experience.

I believe that whole community would love to see improvement in this area as writing guard let for self became like a mantra. We can improve this situation by compiler warnings and better tooling or/and improving the syntax of closures to lesser the problem.

I would be personally satisfied if at least for closures that returns Void we could solve it by the syntax sugar in a form that was already proposed few times. For any other closure I don’t think we need any additional syntax as probably it won’t be significantly shorter and more descriptive than typing guard inside the closure as we use to now.

{ [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

Hi all, thanks for all the feedback so far. To bring further discussion back on track and to summarise:

  • There appears to be wide spread frustration with the current capture list syntax – particularly in regards to class types.
  • Common Cocoa APIs and idioms frequently and pervasively require developers to utilise non-owned callbacks to perform tasks without use of a retain cycle. It seems likely highly that the majority of capture lists 'in the wild' are of this variety.
  • The inline method of passing these callbacks often results in unclear and aesthetically dissatisfying code, with common and repetitive boilerplate that – apparent from these discussions – many developers have an aversion to.
  • The idiom of factoring out these inline methods to instance methods, and passing the instance method instead – whilst cleaning up the call-site – silently creates a strong reference that, from these discussions, clearly many developers aren't expecting.
  • The 'correct' syntax for factoring out these instance methods requires an intermediary forwarding closure of the same arity as the instance method that, I assume, must appear again and again in idiomatic Cocoa applications – and I imagine any code that makes use of class types and asynchronous routines.

Whilst there seems to be some argument on the solution, there is no doubt appetite for
a solution.

5 Likes

In the interim – derived from the ideas of @sveinhal above – I've come up with something that I'll likely use in my own code to clean-up call sites where these callbacks occur.

It's not ideal as 1) it requires any class that uses it to conform to a protocol to gain access to the functionality (we can't extend AnyObject), and 2) it requires a method per arity as the obvious way using generics leads to a retain cycle.

Callbacking

protocol Callbacking: AnyObject {}

extension Callbacking {
    
    // Nullary methods

    func ucall<T>(_ classMethod: @escaping (Self) -> () -> T) -> () -> T {
        return { [unowned self] in classMethod(self)() }
    }
    func wcall(_ classMethod: @escaping (Self) -> () -> Void) -> () -> Void {
        return { [weak self] in
            guard let self = self else { return }
            classMethod(self)()
        }
    }

    // Unary methods

    func ucall<T, A1>(_ classMethod: @escaping (Self) -> (A1) -> T) -> (A1) -> T {
        return { [unowned self] a1 in classMethod(self)(a1) }
    }

    func wcall<A1>(_ classMethod: @escaping (Self) -> (A1) -> Void) -> (A1) -> Void {
        return { [weak self] a1 in
            guard let self = self else { return }
            classMethod(self)(a1)
        }
    }
    
    // further n-ary methods omitted for brevity
}

Usage

From:

animator.addAnimations { [unowned self] in
    self.myFactoredOutAnimationInstanceMethod()
}
animator.addCompletion { [weak self] in
    self?.myPostAnimationCleanupInstanceMethod($0)
}

To:

animator.addAnimations(ucall(Self.myFactoredOutAnimationInstanceMethod))
animator.addCompletion(wcall(Self.myPostAnimationCleanupInstanceMethod))

And here's a version up to 20-arity for those who'd find it useful: https://gist.github.com/tcldr/174f43f6e39379bc5f0cda686ac46f84

3 Likes

FWIW, you can pass self and do it with standalone functions so you don't need to add the protocol adherence:

func ucall<Self: AnyObject, T>(_ passedSelf: Self, _ classMethod: @escaping (Self) -> () -> T) -> () -> T {
    return { [unowned passedSelf] in classMethod(passedSelf)() }
}
func wcall<Self: AnyObject>(_ passedSelf: Self, _ classMethod: @escaping (Self) -> () -> Void) -> () -> Void {
    return { [weak passedSelf] in
        guard let passedSelf = passedSelf else { return }
        return classMethod(passedSelf)()
    }
}

// Unary methods

func ucall<Self: AnyObject, T, A1>(_ passedSelf: Self, _ classMethod: @escaping (Self) -> (A1) -> T) -> (A1) -> T {
    return { [unowned passedSelf] a1 in classMethod(passedSelf)(a1) }
}

func wcall<Self: AnyObject, A1>(_ passedSelf: Self, _ classMethod: @escaping (Self) -> (A1) -> Void) -> (A1) -> Void {
    return { [weak passedSelf] a1 in
        guard let passedSelf = passedSelf else { return }
        return classMethod(passedSelf)(a1)
    }
}

// Usage:
animator.addAnimations(ucall(self, Self.myFactoredOutAnimationInstanceMethod))
animator.addCompletion(wcall(self, Self.myPostAnimationCleanupInstanceMethod))

slightly less concise, but less steps to use it.

2 Likes

I've chewed on this for a few days and I've come to two realisations:

  1. [...] in Swift just feels wrong every time I write it.
  2. Just like !, unowned feels like a disaster waiting to happen (because it usually is).

Specifically, what exactly do we want with unowned other than to skip writing guard... when we know it's safe. That doesn't fee Swifty to me.

I'd go with that - strong for non escaping, weak for escaping. No options.

3 Likes