Hi Swift Evolution,
During the discussion on the acceptance thread of SE-0279, the core team described how SE-0279 does not rule out future proposals aimed at improving the state of trailing closure syntax, and encouraged further discussion of follow-on proposals be broken out into new threads on the evolution forum.
One potential follow-on proposal is currently being pitched. But the design space for these follow-on proposals is large, and not all have the same characteristic of keeping future directions viable. The core team thinks a wider discussion of the different alternatives would be better had prior to moving any of them to the pitch phase.
To help guide this discussion, the core team thinks it would help to set some principles against which these proposals (or more generally, any change to the language) could be evaluated. Below are three principles the core team thinks are key to evaluating these proposals. For the most part, this is consolidating advice the core team has previously given, in the SE-0279 thread and elsewhere. We would love the community’s feedback on these principles, as well as any additional principles that should be considered.
Principles for Evolution Proposal Evaluation
Swift is now an established language, and there are limits to what changes we can accept.
Source compatibility between different versions of the compiler must be preserved, without any need for migration in all but very rare circumstances (such as fixing serious correctness bugs). This is critical for use cases such as Swift packages, playgrounds, or ABI-stable module interfaces, where even a tool-assisted migration is not viable.
In order to allow source-breaking changes, major versions of the Swift compiler may provide compatibility-preserving language version modes. This allows for adoption of new syntax over time. But these version modes should still be used sparingly. Because source compatibility must be preserved, each version must continue to operate indefinitely. This runs the risk of creating "old" and "new" styles of Swift, both of which may exist alongside each other for long periods of time. Having to apply changes may also discourage users from updating to the newer language version, inhibiting adoption of other features.
The impact on API surface of these versioned source breaks must also be considered. A change to Swift may encourage a “new” style of API that makes many existing APIs look outdated. This in turn may require APIs to churn more rapidly, causing knock-on problems in areas such as maintenance burden of the old and new styles of API, outdated documentation and stack overflow answers, or a need to bump major semantic versions in packages.
For these reasons, source-breaking changes, even versioned ones introduced with a migration plan, must clear a very high bar in terms of the value they add. Most source breaks can be worked around with enough energy in the compiler. So a proposer may feel they can break source if they feel the result is better, and it becomes a debate about whether it is better. But this is secondary if it’s only marginally better, if it runs the risk of bifurcating the language over a long migration.
A proposal to add language-level asynchrony to Swift is likely to render many callback-based APIs obsolete. However, the dramatic improvement in user experience brought by such a feature would certainly be considered worthwhile despite this burden on API authors to update their code to the new style. So long as the old-style callback APIs remain available, no source breakage should occur.
subscript currently deviates from regular function declaration, in that parameter names are not argument labels. Instead you must use explicit argument labels. This is confusing, and might be better made consistent, but would cause extensive source breakage. While this could be language versioned, it would cause considerable source churn and block adoption of other Swift features in the same version. The consistency benefits are therefore clearly outweighed by the cost.
Control of the Call Site
Swift tends to leave discretion about the appearance of the call site, and especially the use of a label, to the the API author rather than the caller.
This principle is key to helping API authors preserve clarity at the point of use. If there are multiple ways a client can choose to call your API, all possibilities must be considered and designed for, making the design of a clear API harder.
As stated in the acceptance of SE-0279, the core team is not satisfied with the basic design of trailing closures in Swift today, particularly in the way it deviates from this principle. If we were free to redesign the language without restrictions, we would likely do this feature differently. A more consistent design could, for example, only permit trailing closures at all when the API author uses a label of
_ for the closure. But as outlined above, changing to such a design at this point would run counter to Swift’s principle of source stability.
More control at the call site also provides for more accurate method lookup, and therefore more predictable behavior and faster compile times, all things critical to the Swift developer experience.
Prior to Swift 3, callers could supply defaulted arguments in any order. SE-0060 removed this ability, requiring arguments appear in the order of the function declaration. This put ordering under the control of the API author, and increased consistency at the call site.
Language Consistency and Future Directions
Evolution proposals should be consistent with the existing language. They should leave open further complimentary proposals.
The proposal for SE-0279 describes its design for multiple trailing closures as an extension of the existing rules for single trailing closures. With both single and multiple closures, the first trailing closure has no label. In the case of multiple trailing closures, additional labeled closures can then follow the initial unlabeled closure.
It also left open other future directions. Mandatory or optional labels, or enforced or banned trailing closures for a particular function, remain as feasible as they were with single trailing closures (which is to say, still not especially feasible but not ruled out).
This is a good combination: extending existing patterns in the language as an improvement that stands on its own, without requiring or closing off future directions. Depending on how these play out, SE-0279 is either the final state of this part of the language, or an acceptable resting state that we move on from in a later release. Either of those is an acceptable outcome.
By taking this approach—of extending the language in a consistent way while leaving open future directions—we are able to continue to evolve the language over time.
The original proposal for SE-0279 introduced an “outer brace” syntax for multiple trailing closures. Unlike the amended proposal, this did not extend an existing language pattern but instead introduced a new alternative calling syntax. It left no real path for resolving the discrepancy in the future.
SE-0255 extended the existing precedent of implicit return for single expression closures to be useable in all functions. As such it was a consistent extension of the existing language. It left open the future possible direction of building on it with a complimentary proposal making
switch statements into expressions. Such a proposal would complement SE-0255, but SE-0255 stands alone without this proposal as a useful improvement to the language in itself.
Applying the principles to trailing closures
In accepting SE-0279, the core team stated that it felt that the proposal did not block the path for future proposals around the labeling of trailing closures. But at the same time, we considered the path for several of those follow-on proposals to be challenging.
These principles are meant to give a framework for evaluating those challenges. Given the above principles, how might some of the candidate proposals regarding trailing closures be evaluated? Some possible proposals---including one that has been worked up as a full pitch---are evaluated below.
The application of these principles is not meant to be absolute. As shown especially in the source breaking case, a principle can be overridden if enough motivation is shown. But that motivation must be along the lines of significant new functionality or elimination of significant active harm.
The core team would welcome feedback on the principle application below, as well as any comment on the principles themselves, or further principles for consideration.
Mandating argument labels by default
A proposal to mandate the use of labels on trailing closures where the argument has a label would incur significant source breakage. So at a minimum this change could only be introduced through versioning, with a potentially lengthy migration period needed for callers to update their code to use the label. Many APIs have been designed with a label, but assuming the label will usually be omitted, and may need to be updated to avoid callers needing to supply an unnecessary label. Since this change would not bring significant new functionality to Swift, it would be hard to justify on this basis.
Allowing optional labels
Allowing the caller to optionally include a label for the first trailing closure has been pitched as an enhancement on top of SE-0279. This pitch has the benefit of allowing multiple closures of equal importance (the canonical example being
Binding(get:set:)) to label both closures. But giving this option to include a label or not at the call site to the caller violates the principle that the API author should control the appearance of the call site.
Controlling trailing closures via
During the second review of SE-0279, it was suggested that multiple trailing closures be introduced using
_ on the label to control if a trailing closure were allowed, or to require the label on the first trailing closure. This could only apply to multiple trailing closures: single trailing closures would still have no label. This avoids the problems with source stability (multiple trailing closures are brand new) and with giving the API author control. But it leaves the language in an inconsistent state, and making this decision around multiple trailing closures would force the language down a specific path if we also wanted to solve the problem for single trailing closures.
Controlling trailing closures via an attribute
The introduction of an attribute to block the use of trailing closures on specific APIs is another potential solution to the
Binding(get:set:) problem. While not as consistent as the use of
_ would have been, it does return more control to the API author. The use of this attribute would have to be versioned to avoid a break in source stability, but could be used as a guide to IDEs for code completion even in earlier language versions.