Principles for Trailing Closure Evolution Proposals

I think this is an important and realistic aspect to take into consideration, and this is the first time I see it addressed, but I don't think progress should be dictated by businessmen's purse string. The language's direct audience is the programmers, not managers or businessmen, so the priority should be making programmers (especially the new ones) happy, then their managers.

Strict source compatibility doesn't always magically reduce the cost of maintenance, either.

If Swift never fixes design mistakes through some source-breaking changes from time to time, it will become painful to work with as mistakes pile on. Introducing workarounds instead of addressing the mistakes directly further complicates the compiler, and will make it more error-prone. The more cumbersome it is to work with a language, the less efficient each programmer can be, the longer it takes to implement changes, the less a manager gets for each dollar spent.

If one day Swift becomes unswifty, similar to how Swift sprung from the misery of coding in C++, new languages will come out in hopes of fixing and replacing Swift. If or when this happens, fewer people will code in Swift, and it will make existing codebases harder to maintain. I know a large company that still maintains an old FORTRAN codebase. It spends a fortune to keep the original engineers around, because no one else can or is willing to work on it. At the same time they are migrating to a new codebase written in relatively modern languages, and it has taken years and costs a lot of money too.

You'll always pay the cost of maintenance, and it's just a matter of when and how much. Without any detailed analysis, there is no way to be certain whether source stability costs more or less down the line, and the answer is likely project-specific. The only way to avoid any costly maintenance is by abandoning your projects altogether, but then they just become sunk costs, which are still costs.

And because I can never pass on a good meme opportunity: Money is temporary; the glory of Swift is forever.

13 Likes

I can’t speak to server/linux use, but certainly in the Apple platform ecosystem enterprise adoption of Swift is being driven by the increasing difficulty of recruiting people willing to work on large Objective-C codebases, not a perception that Swift is a more stable and cost-efficient alternative.

This rather suggests that developer ergonomics is more significant to Swift dominance than bean-counter-friendliness.

10 Likes

I really don't think that is relevant here:
If you are doing iOS, you use Swift — if you doing something else, you don't.
Of course, that's not really true (you could as well use Flutter for iOS ;-), but when you put away the rose-colored glasses, Swift simply isn't a viable option for many projects (I wish it was — but when I leave the iOS-bubble, I hardly see anyone using it).

Backwards compatibility is a very important aspect — but there are enough options for those who want to keep using code from twenty years ago, so Swift will probably never be chosen because of this reason.

Whereas others (C++, Windows…) had (and have) a strong focus on stability, this has never been a reason to stick with Apple for me. Actually, I think favouring innovation over stability is what made the company what it is now.
If you really believe in world domination, Swift is just starting to tackle this goal — and when you block improvements in this early phase, we'll very soon have a niche language full of cruft.

So I say don't applaud core for making managers happy — encourage them to push Swift forward boldly instead!

18 Likes

To be clear, what the core team has enunciated are and must be taken as principles and not (or at least, not all) as red lines.

In the case of trailing closure evolution, the principles are mutually contradictory, so no design for multiple trailing closures (not even SE-0279) would be inside all the lines:

  • Existing syntax does not allow for control of the call site and any new syntax should not extend the problem
  • Any new syntax should be consistent with existing syntax for language consistency
  • Changing the existing syntax to allow for control of the call site cannot be done for source stability reasons

Only the source stability requirement is spelled out as a must, and since the core team feels that there is at least a "path for future proposals around the labeling of trailing closures," then the remaining principles must represent guideposts or ideals that we attempt to maximize.

For that reason, I feel that optional initial trailing closure labels, the elision of which would be a warning as opposed to an error, maximizes along all the axes listed above.

Adding a new attribute (particularly one that allows minimum Swift language versions) would be a more elaborate design that, like the original "outer brace" syntax for SE-0279, does not "extend an existing language pattern but instead introduce[s] a new alternative"—which is less ideal along the language consistency axis.

Not doing anything (the status quo) would be less ideal along the control of the call site axis; as some have explained, it would mean that elision of any of the trailing closure labels could continue to be chosen without the author even being warned about it.

Along a continuum, a warning for existing code could be considered "less" source compatible than no warning, but as far as meeting the must criteria for source stability (a binary yes-or-no criterion), the core team has said that they consider warnings to be source stable.

38 Likes

I agree that this seems to be an optimal path forward given the constraints. A warning leaves the label optional but recommended. The label will be required when warnings are treated as errors. This allows teams who choose stricter enforcement to have it provided by the compiler rather than an additional layer of tooling (where it is more difficult to implement and less likely to be 100% accurate).

10 Likes

I 100% agree, this is by far the best possible solution, given the principles, and the vast majority of the community interested in this seemed to agree.

I find the OP gives extremely useful guidelines, and has great value in itself. The issue of the bar to breaking changes being too high is complex, and honestly might require a separate discussion than this, which is mostly related to multiple trailing closure.

That being said, an optional first label that emits a warning (future error) if omitted is the correct solution here, and I didn't see any strong counterargument of any kind to that. The warning is the right tool here, because it's at the exact crossroads of

(which it doesn't completely, due to the warning) and a breaking change, exactly what you want in solving a situation that's a mess in the first place. I don't feel like adding much more to this discussion, it's honestly stretched too far. We identified, with long posts and strong, mostly uncontested arguments, the best solution, all things considered.

6 Likes

A change to Swift that mass-generates warnings from code that was perfectly acceptable previously is essentially a source break. It is a soft break, similar to how language versioning a break is, rather than a hard break, but it is still a break and so still conflicts with the source compatibility principle.

To requote from that principle:

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.

and from the interpretation of it regarding trailing closures:

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.

1 Like

Right. But the must under the principle refers only to what you term "hard breaks." Either the existing code compiles or ceases to compile, and we don't want to entertain proposals that cause working code to cease to compile.

Everything else is subject to evaluation along the various axes articulated; along each axis, a design may be great or not-so-great, reflecting the various tradeoffs that we have to make in balancing priorities.

When it comes to trailing closure syntax, since the current design does not place control of the call site in the hands of the API author, precisely how much we progress in terms of advancing that goal is how much we lose in terms of preserving source compatibility.

Therefore, these are not orthogonal axes at all, unless we take the design in a new direction (i.e., implement some totally new feature), thereby giving ground along the "language consistency" axis. Put another way, there are three goals articulated here, but at most only two degrees of freedom.

18 Likes

To make it clear and understandable for me:

  • Does this mean that source breaking changes are off the table now and forever?
  • If not, what kind of change would be feasible to justify a breaking change?
  • Iff there is a breaking change scheduled, would that be a green light for other breaking changes that were off the table before (e.g. if there is breakage anyway why not do otherwise unjustified breaking changes in the same wash)?

or is the whole discussion moot since there won't be any breakage, never?

1 Like

Hello Ben,

I think we are pushing the churn and dissatisfaction with the constant breaking changes in the Swift 1-3 era a bit too far here.

I think this “developers are demanding no source breaking changes” seems (and now no soft breaking changes are also what we demand), sorry to say it like this, questionable or valid to question the basis for this absolute. Based on what evidence? If the evidence is the Swift 1-3 days this is a bit of an over reaction...

Especially when there are either changes to real pain points in the language or oddities (like stores closures not being able to use argument labels... aka big part of what has always been part of good call site readability in Swift).

19 Likes

I’d like to inject some anecdata here…

Over the past few years, I’ve been one of the main drivers in bringing a million-line Objective-C codebase to a point where Swift is now the preferred language for new code.

When we started this work, upgrade churn was one of our biggest concerns; doing a big-bang upgrade of the entire project, with code owned by many teams, would be infeasible. After the ability to upgrade one module at a time was introduced, this went from a top-three concern to well off the top ten.

If I was making the same evaluation for today’s Swift, but without source stability, it might possibly be in the lower end of the top ten.

11 Likes

Exactly this. Thanks to ABI stability and module stability, source stability is no longer as important in the teams I've worked with (codebases big and small). Especially, with support for binaries in SwiftPM, some of the most critical dependencies no longer require us to recompile everything from scratch with every new Swift version and to complain about breakages. That dependency can be stored as a binary as long as possible, while every other module (even when not ABI-stable) can evolve at their own pace in terms of source compatibility.

Even on the platforms that don't have ABI stability (basically every non-Apple platform at this point), the time to make Swift clean and consistent is now, before those platforms get more significant adoption. At a later time changes would be more painful. And again, ability to specify Swift version per package in package manifests means it's not as painful, just a minor annoyance if anything in most cases.

25 Likes

Which is why is a fair question to ask “why is source so important that the bar is so so high?” even for warning which I am not sure I agree we can rebrand them as soft breaks (which is in itself only an issue because it is not easy for devs to add ignore flags for such warnings? That is something I think would be pragmatic to allow people to do easily even though it goes against an opinionated choice...

never had issues with Objective-C to just to -Weverything and -Werror and then either, as a team agreed choice, to locally add some pragma clang diagnostic push and pop or ignore some warnings as specific compiler flags. This is an area where I cannot agree with the current “simple” but “not easy” opinionated choice)

11 Likes

I don't see how this is true. The "essence" of a source break is twofold:

  1. impossibility of compilation, or
  2. code compiles but behavior (semantics) changes.

Warnings (with the possibility of a very, very easy automatic migration) on code that compiles and behaves the same are not source breaks in any way, and are actually a very useful tool precisely to compensate past mistakes that we might want to (slowly) correct over time.

It's true that "Many APIs have been designed with a label, but assuming the label will usually be omitted", but this is exactly the past mistake we have a chance here to correct. This point is true, but also the fact that a warning (used responsibly, but this seems such the case) helps to correct the mistake is true: the two are in opposition, there's no clear winner, so we must decide without it. And the vast majority of the people interested in this are for this addition to multiple trailing closures.

The resistance of the core team towards this is simple addition to multiple trailing closures is starting to become a little silly, because the arguments (up to this point) are being clearly identified by the community as very weak. And the discussion has become tiresome to follow: I don't remember anything ever happening here of this proportion.

15 Likes

I’m trying hard to see an endgame where we can do anything here to significantly improve what is a rather awkward spot in the language. I feel the principles here are so tight and locked that the only solution is another attribute, which is really disappointing. I don’t feel like “add more attributes” will scale, or make Swift a better language. Indeed, I see it as a very poor sign for the design space of the language, and perhaps a sign that if we’re not willing to make a source-warning change here, then we should leave well enough alone.

My question would be, however, what do we see the outcome being, ideally, for this change? We want delineation by the api author of intent for API naming. When this happens, would we not expect these warnings to start cropping up anyway? And would not the biggest source of these be in the Standard Library, which we all use? We won’t be able to escape these warnings anyway if they’re in the standard library - they’ll be intentionally there.

I guess I’m just trying to see other alternatives and whether they would be better? I’m not 100% sure avoiding warnings is a good idea, especially if you’re deliberately handing control back to the API author, who will want those to enforce their preference.

6 Likes

What if it's optional label with a warning using compiler flag? That code bases can opt-in to/out-of.

Swift has no optional warnings ATM, so it's unlikely that would be an acceptable solution.

No. The position is clearly stated that source breaks (so long as they are versioned) are very much on the table when they add significant value. It's just that, in the current judgement of the core team, changes like mandating labels on existing functions does not deliver that level of added value.

The introduction of language-native asynchrony is the example given in the OP. This would deliver significant value to Swift users and that would definitely be enough reason to make source breaking changes to the language to make it fit well. It's not clear if this is even necessary — we need to get into the design process — but source-breaking changes are certainly on the table if needed.

Deprecation of individual API are also very much on the table when, for example, it's found they are causing active harm. Those should be considered separate from language-level breaks that affect APIs outside of the standard library, as opposed to specific targeted breaks within the standard library.

Here is another example of an acceptable language-level break. Swift recently started warning about this code:

let myArray = [1,2,3]
let ptr = UnsafePointer(myArray)
// Warning: Initialization of UnsafePointer<Int> results in a dangling pointer

This is merited because the code is fundamentally unsound and can result in undefined behavior. Over time, perhaps this should become a compiler error rather than a warning.

Can I turn this question around and ask: why does this matter? What we do know is such an opportunity is not coming soon, even if it isn't ruled out in the future. Therefore why is it worth expending energy ruminating over potential breaks we know are not on the horizon, instead of on one of the many urgent or important topics on which both the core team and the community could be spending energy?

I feel like I mostly understand the conversation about how high the bar is for source stability, even if I think the bar should be lower, but what makes changes like renaming first(where:) to firstWhere(_:) an easier thing to justify along the same criteria than a change to requiring call sites to use first where: { ... } if they were using the existing trailing closure syntax? I’m referring to the thread on renaming stdlib functions that was posted when multiple trailing closures were being discussed.

3 Likes

"No language dialects" is a fairly long-standing principle in Swift's design that probably belongs on the list above.

3 Likes