Using the principles stated (ignoring source stability for now), it seems obvious that trailing closures in their current state have design flaws. The ideal solution seems to be that a function foo(body: () -> Void)
(#1) cannot be called with a trailing closure that omits the argument label while foo(_ body: () -> Void)
(#2) can. This is intuitive as it makes the labels for closures work along the lines of how regular argument labels work in Swift. Also, this returns the author of code control over their call-site.
Circling back around though, this of course does violate the principle of source stability. All code invocations to #1 in code wherein a trailing closure are used immediately become invalid code. This sucks because it puts us between a rock and a hard place, we want to guarantee that old code continues to compile, while we can't do that if we fix the behaviour how I outlined.
Is there any way to feasibly get around the probably of source stability now and in the future? I don't like that we are held back by "we made a bad design decision in the past and now we have to live with it forever." Is there any way that we could specify compiler versions on certain project and modules to remedy the issue? More generally, is there any way to be able to introduce breaking changes while upholding the integrity of older codebases that were made with different versions of Swift. Otherwise, it seems that the idea to add in breaking changes every few years is the best one. While it means that older codebases have to update to get the new features, it allows us to truly fix mistakes in the language rather than be weighed down by them forever. Also, this could be made substantially easier with a good migrator.
I view Swift as a modern language that really improves expressivity beyond other programming languages as there are many things that we got right from the get go, but I fear that for the sake of making old code work, we sacrifice the true potential of the evolution of the language. Although we are encountering the burdens of source stability now while shaking our heads at prior design choices, this definitely won't be the last time it becomes an issue when we are trying to fix Swift's design. As such, I think introducing a framework for including important source breaking changes like this is essential, whether that be releasing a source breaking versions of Swift every X year, finding a way to use modules written in a different version of Swift or something else. Swift is quite a young language in the grand scheme of things and us sacrificing its features for decades to come just based off of the few years Swift has emerged seems silly.
I was thinking about being able to interoperate between compiler versions of Swift. While I certainly am not an expert in the subject, I see some problems come to mind straight away. Take trailing closures for examples, if we were to make the aforementioned changes to make their behaviour consistent with other argument labels, many call-sites would be rendered invalid. It is suggested in the original post to make an attribute that requires closures follow these rules, but I think that an opposite attribute would maybe be in order. Instead of making the right behaviour require an attribute, we could make the old behaviour annotated with an attribute. So if I am writing a codebase in a new version of Swift using a package/module/project written in Swift 5 for example, all functions that can have trailing closures would be imported to the new project under an automatically applied attribute specifying that it can omit the label for a trailing closure (maybe @canOmitLabelInTrailingClosure
). The ideal outcome of this is to make the old behaviour more burdensome to keep around than the new one, thus incentivizing the use of the better behaviour, not the old one. This is ideal as it makes the old behaviour an edge case while making the new streamlined. This also seems to fix the issue of source stability. Maybe this is a viable solution, if it is, it seems that we'll be able to have our cake and eat it too. Are there any other important underlying problems to a solution like this that I'm missing or that would need to be addressed?
Alternatively, we can go down the route of including breaking changes every X years. In the OP, it says that source breaking changes must have substantial weight behind them. I believe that in the case of trailing closures its current design is harmful and making it consistent with the language would have clear improvements. Unlike OP though, I don't really view a ton of uses of a bad design choice to justify keeping that bad design around in perpetuity. Rather, I feel that it is something that should be fixed ASAP so that we don't have more uses of a badly design feature before it can be remedied.