Thank you for the thoughtful review. The migration story you lay out does indeed maintain source compatibility better than the proposal under review. I've been experimenting with doing both scan directions and warning if the backward scan was chosen. For example, this allows the compiler to handle pick between the two based on what works:
func trailingClosureEitherDirection(
f: (Int) -> Int = { $0 }, g: (Int, Int) -> Int = { $0 + $1 }
) { }
trailingClosureEitherDirection { -$0 } // only forward scan works, choose that
trailingClosureEitherDirection { $0 * $1 } // only backward scan works, choose that
For the latter case, there is a warning with a Fix-It to point out that you're doing something that will break:
warning: backward matching of the unlabeled trailing closure is deprecated; label the argument with 'g'
to suppress this warning
trailingClosureEitherDirection { $0 * $1 }
^
(g: )
For functions where both scans produce meaningful results, I biased toward the forward scan (i.e., the model we want in Swift 6), producing the warnings shown in this message.
This approach reduces the source-breaking impact of SE-0286 as written (ModelAssistant and SwiftUI-related failures from the source compatibility suite pass with this change), but doesn't really change the proposal itself---it's in the realm of backward-compatibility tricks we tend to do with any Swift release. However, some programs can still break with this scheme, because some amount of code that type-checked as backward will type-check differently as forward.
Note that nearly the same implementation can be used to bias toward the backward scan, matching your migration story. If we go that route, I would like us to be certain that we won't fall into the same trap that SE-0279 did, where we provided great source compatibility but left behind a feature that didn't work as intended. All of the examples from the proposal work properly when biasing toward the backward scan, albeit for different reasons:
UIView.animate(withDuration:animations:completion:)
works because only the forward scan comes up with an answer. This style of API---required closure first, followed by optional ones---fits well with the forward scan.View.sheet(isPresented:onDismiss:content:)
works because the forward scan's heuristic skipsonDismiss
, so the forward and backward scans produce the same result.BlockObserver.init(startHandler:produceHandler:finishHandler:)
works because the type signatures of the various closure parameters are different enough; especially important is that the first and last closure have a different number of parameters, so the type checker can decide on first vs. last. The same occurs with SwiftUI'sTextField.init(_:text:onEditingChanged:onCommit:)
.
I'm somewhat concerned that we've been getting lucky with that third bullet. Is there a class of APIs out there that will be hard to write properly to deal with the mix of forward and backward scanning rules?
Your suggestion does (necessarily) retain the weakness that the unlabeled trailing closure can move from the "last" closure (when the backward rule applies) to an earlier closure (when one uses multiple trailing closures). However, at least the compiler will produce a warning in the former case.
[EDIT #1: I implemented @xwu's suggestion. It's going through testing now]
[EDIT #2: Toolchains for macOS and Linux are now available to implement @xwu's suggestion]
Doug