Hi @jrose, thanks for your feedback, it's appreciated! Let me attempt to articulate my feeling on the matter:
I think @GetSwifty is right with their observation that the particular area of friction revolves around the interplay of objects and their own instance methods, or the instance methods of their owned members.
Also, it seems to me that by far the most common capture lists will be either [unowned self]
or alternatively [weak self]
. I certainly don't have the stats, but let's assume that the odds of a capture list looking like the above is > 60% – then I would suggest an elegant shorthand for the syntax might be something adopters of the language would appreciate.
In terms of use-cases, in the world of UIKit and Cocoa, the first API that comes to mind is the Animation APIs.
Frequently, these calls require the setting of a dozen or more properties with the various values often coming from the existing state of the instance. Lots of expressions containing self
– often multiple times in one line – these closures can be particularly dense when we're talking about a weakly captured self
.
A short example adapated from the current project I'm working on:
private func finishNavigationTransition() {
transitionCoordinator?.animateAlongsideTransition(alongsideTransition: { [unowned self] context in
self.updateContentAreaSubject(withViewController: self.topViewController) // double call to `self`
self.baseView.layoutOverlayIfNeeded()
self.baseView.backgroundColor = self.style.backgroundColor
self.baseView.transform = CGAffineTransform(scaleX: self.style.outScale, y: self.style.outScale) // triple call to `self`
self.baseView.alpha = 0
...
}, completion: { [weak self] context in
guard let self = self else { return } // guard for self required
self.bindNavigationHeaderViewToChildViewController(self.topViewController) // double call to `self`
...
})
}
This is the simplest of animation examples.
Consider a more complex animation requirement, with nested keyframe blocks, many calls to self
. If it's a weakly held closure maybe some optional coalescing gets thrown in – at least until you need a parameter from an instance member - at which point you'll need to rewrite with a guard
... As any Cocoa dev knows, animation closures can be far more complex, many scores of lines long.
I think it's fair to say at this point that the sensible thing to do is break out a complex closure like this into its own instance method:
func finishNavigationTransition() {
transitionCoordinator?.animateAlongsideTransition(
alongsideTransition: { [unowned self] in self.transitionAnimations(context: $0) },
completion: { [weak self] in self?.transitionAnimationCompletion(context: $0) }
)
}
func transitionAnimations(_ context: UIViewControllerTransitionCoordinatorContext) {
updateContentAreaSubject(withViewController: topViewController)
baseView.layoutOverlayIfNeeded()
baseView.backgroundColor = style.backgroundColor
baseView.transform = CGAffineTransform(scaleX: style.outScale, y: style.outScale)
baseView.alpha = 0
...
}
func transitionAnimationCompletion(_ context: UIViewControllerTransitionCoordinatorContext) {
bindNavigationHeaderViewToChildViewController(topViewController)
...
}
It is indeed a few more lines vertically, but with a complex and lengthy routine the space you add vertically is made up for by the space you save horizontally – which in my opinion also makes it far clearer.
But now we see the side-effect of these common refactors:
{ [unowned self] in self.transitionAnimations(context: $0) }
and { [weak self] in self?.transitionAnimationCompletion(context: $0) }
.
These frequent single line closures – simple trampolines to their associated instance methods – seem pervasive in most Swift code bases.
Most of the syntax here feels redundant – especially when you consider the seductively elegant but retain-cycle creating alternative:
func finishNavigationTransition() {
transitionCoordinator?.animateAlongsideTransition(
alongsideTransition: transitionAnimations, completion: transitionAnimationCompletion) // Whoops. Reference cycle!
}
In fact, I'd argue that until you're well versed with swift, this is exactly what you will end up doing – especially as the compiler remains silently complicit.
In addition, I would say precisely because this idiom exists we must add in the non strongly held counterparts – to mirror the syntactic expressiveness available when a strongly held reference to self isn't an issue.
private func finishNavigationTransition() {
transitionCoordinator?.animateAlongsideTransition(
alongsideTransition: @unowned(transitionAnimations), completion: @weak(transitionAnimationCompletion))
}
The technical reasons, I wouldn't dare to comment on, but from an ergonomics perspective I hear the argument about we should really be using full label lists anyway (that's a -1 from me, only to disambiguate) but seeing as the syntactic precedent has already been set, my own feeling is we should be consistent in its support.
There's a real elegance to being able to pass a function in this way – it would be a shame to lose it.
So in summary, I think the point is this:
- There are two abundantly common capture lists within closures;
[weak self]
and [unowned self]
. (My guess would be at least 60% of capture lists are one or the other.)
- Often times, said closures contain a call to a single instance method that has been factored out to aid the readability/line-length of a routine due to numerous calls to
self
– often multiple times per line. Or to a avoid a 'pyramid of doom' of nested closures.
- The most intuitive and ergonomic syntax (passing the instance method directly) causes 1) a retain cycle and 2) offers no warnings from the compiler that this might be an issue.
- There exists a syntactic precedent of allowing functions without labels to be passed as closures almost everywhere else in the language – it seems unintuitive to prevent that here.
So, in the interests of mirroring existing capabilities and as a nod to the frequency in which this pattern in encountered, I would say – if not via a prefixed keyword – there is a strong argument to resolve to come up with a way to elegantly pass instance methods as closures with unowned or weakly held references to self
.
I hope this clarifies my thinking somewhat and thanks again for feedback so far.