Upon Swift 6: Solve inconsistency within the language

The arrival of Swift 6 means a chance for “regretting” some language designs with API breakage — after 3 years which is fairly a long time for Swift and the Swift community. With the Swift 3.2 and Swift 4.2 effort, transition to a breaking Swift release has proved to be a lot smoother.

I would suggest we pick up some of the deferred breaking pitches, which intended to eliminate inconsistency within the language. These ideas have already received positive feedbacks from the community, and yet didn’t make their ways into reality.

Naming Inconsistency: Revisit SE-0132

SE-0132 proposed to rename and redesign the existing Sequence snd Collection APIs by rationalizing them according to the API Design Guidelines.

The Core Team did regard these changes as “too expensive”, but I believe not fixing them as soon as possible would become the really expensive thing. Such confusing naming from the standard library is a significant problem which will impact all the future users.

Given ABI stability goal added in Swift 5, we’re already suffering more to fix it. I would suggest how we deal with SwiftPM 4 manifests: add a libStdlib5 fallback in future releases for compatibility issue, until we finally decide to drop it (through future evolution). I’m not an expert in ABI though, if there’s any problem welcome to point out.

Links:

Design Inconsistency: throws overload

throwing/non-throwing overload is the new inconsistency introduced by the amendment to SE-0296. As Chris pointed out:

There are, of course, considerations regarding source breakage on introducing this in Swift 5. For a healthier language design, there’s no better opportunity to add (and enforce) this in Swift 6. The Core Team decision addressed:

Links:

Behavioral Inconsistency: Eliminate implicit initializations

Last week, Jordan Rose (@jrose) pitched to remove the implicit initialization of Optional variables and received heated discussion, including suggestions on extending the pitch to include PropertyWrapper.

Behavioral inconsistency like implicit initializations is more subtle, yet has potential to cause bigger problems because they burden users with kinds of “differences”. As Jordan explained himself:

In fact, this piece of change has already been pitched four times since 2017. It’s time to get it fixed now.

Links:

Others

I believe there are more inconsistent cases which l’m not able to cover, so welcome to address them in other threads (let’s keep this thread focused). I would like to call for the whole community to push these efforts and make consistency a real goal for Swift 6.

32 Likes

If I had one wish it would be to "fix" protocols in Swift to make them safer to use, by finally fixing:

  1. Dynamically dispatch protocol extension members
  2. Role Keywords to Protocol Implementations to Reduce Code Errors

There are other Swift regrets that could be taken back now too like eliminating fileprivate.

9 Likes

Returning to Swift 2 access control is the dream, but I'm not sure it'll get enough traction.

I'd also like to suggest that, regardless of what else is done in Swift 6, we fix SR-103. Default implementations of protocol requirements on a base class absolutely should be part of that class's vtable.

7 Likes

Swift 6 does afford us with the possibility of making some source-incompatible changes. I want to highlight the place where you quoted me in regards to SE-0296:

Part of the motivation for such a proposal can be consistency with async .

"Consistency" can be part of an argument to make a source-incompatible change, but it should not be the only argument or even a load-bearing one. Source-incompatible changes have a high bar, because they need to be correcting a serious problem or unlocking some potential that is otherwise unavailable without them.

I'll give an example that is inconsistent that we would not accept: making subscript argument labels behave the same way as function argument labels, so you don't have to "double-up" subscript parameter names to get an argument label:

subscript(row row: Int) -> Double { ... } // inconsistent with func get(row: Int) -> Double

It would make the language more consistent, but it would also cause a lot of code churn where Swift < 6 and Swift 6 have the same code meaning two different things. We would not consider this.

Additionally, ABI changes are not in scope. Swift 6 needs to interoperate with Swift 5 code on a per-module basis, including making it possible for a Swift 5 module to be updated to Swift 6 without breaking clients, and ABI changes work against that goal. We should consider the ABI a fixed entity that will, effectively, never change.

To your specific feature requests:

This one is significantly source-breaking, and would change our basic terminology for working with sequences and collections. I think we are too far into Swift to make fundamental changes like this, and the "harm" from keeping the current set of names is far less than the cost of introducing source incompatibilities of this scale.

As I noted in the thread you quoted, this seems like a reasonable thing to consider. Make sure its justification isn't just "consistency", though.

This one will be interesting, and it's not really about consistency: it's about a one-off rule to eliminate the = nil boilerplate that a number of folks think is problematic. We'd need to understand how much source incompatibility it actually introduces (which, alas, means basically implementing the whole thing), but it seems like it could fit with Swift 6.

Doug

12 Likes

I really would like to understand why it's considered so harmful to rename a few methods. I can't wrap my mind around because I imagine that it would be an easy transition. The old methods could be marked as deprecated, IDEs could suggest the change to the new ones and replace all. I have migrated projects from old versions of Swift and Xcode made it very smooth.
I'm probably wrong about it but I honestly can't see the problem.These inconsistencies could be solved now and avoid the same discussion in Swift 7, 8 and so on.

4 Likes

IDEs can help migrate a code base at a time, but they only work for an actively maintained code base. They don’t retroactively fix up answers on StackOverflow, or example code in a tutorial or on a blog. There is a ton of great Swift content out there, and the overall community pays a cost when we go and change things. Years after the Swift 2 -> 3 transition, you’d still regularly find swift 2 code out there that just doesn’t work.

Additionally, I think there is an overall “budget” for the amount of surface-level change we can tolerate in Swift 6. If it’s too different, especially if it’s “just syntax” that isn’t massively better, it will dissuade users from adopting the new language mode. Even a perfect migration is risky if it causes a large amount of churn in the code base. So, with proposals to make source-incompatible changes, I think we truly have a zero-sum game, where changing a few names over here means we might not get a more substantive change over there.

Finally, this proposal was rejected before. We audit don’t reconsider rejected proposals unless there’s new information that changes the discussion. I don’t think we have that here, and the reasons for rejection (ie, not creating too much churn) are stronger now than they were then.

Doug

24 Likes

One thing I’d like to see relaxed in Swift 6 is Objective-C bridging.

Currently, Swift allows NSString methods to be called on String instances. However, these functions often have better alternatives that are native to Swift:

let foo = "hello ".appending("world")
// use of `+=` or `+` is preferred

And many of these NSString methods aren’t Unicode-correct (instead operating on UTF-16 code units), which is unexpected behavior to many people as native String methods are Unicode-correct.

let bar = "👨‍👩‍👧‍👦".padding(toLength: 14, withPad: "0", startingAt: 0)
// 👨‍👩‍👧‍👦000
let baz = "👨‍👩‍👧‍👦".padding(toLength: 10, withPad: "0", startingAt: 0)
// 👨‍👩‍👧‍�

And these methods return bridged NSStrings instead of native strings, which can be unexpected since there is no indication that they will return a bridged NSString.

Despite this inconsistency and its associated confusion, people still recommend the use of these functions over their Swift equivalents. If you search for “how to pad a string in swift” in Google, every search result tells you to use NSString.padding. (At least, it does for me — Google is known to have a “filter bubble” that changes search results based on the information it collects from you.)

In addition, I think that Swift is mature enough now that we should stop relying on C / Objective-C interoperability behavior for simple things like this. We should remove these kinds of “bridged” methods from native Swift types. If a programmer really wants to use these methods, we should require them to cast the Swift type to the Objective-C equivalent like so:

let hello = "hello "
let helloWorld = (hello as NSString).appending("world")

There should also be a warning in the NSString documentation that most of its methods operate on UTF-16 code units and do not have Unicode-correct behavior.


I’d also like to see some of the Foundation types be re-renamed back to their Objective-C names. Types like NumberFormatter and Timer look like Swift types, but act like Objective-C classes in a way that’s inconsistent with the rest of the language. Also, NumberFormatter now has a native Swift equivalent and Timer has a Combine equivalent. Having two native-looking APIs for the same thing is confusing and should be avoided.

The renaming of these types happened a long time ago (relatively), back when Swift was still dependent on Objective-C for doing almost anything. Now, Swift is more independent, with its own APIs and its own style, and Foundation should reflect that.

15 Likes

Let’s say, if this can go into Evolution again, there would be some milder ways to do the transition. For example, it’s okay to keep the old naming as deprecated (warning), until we finally decide to drop them. ABI could keep the same. There’s no definite large-scale source breakage.

Furthermore, I think SwiftSyntax and the Swift compiler is now smart enough to do such “renaming” migration for us. For any code that can compile with Swift 5, it should be almost painless.

1 Like

We had support for "renaming" migration since Swift 3. It's not a problem of tools, but of deprecating a bunch of existing, well-documented APIs where the only justification is that we don't like the name. But the name we're proposing is one that's already been rejected by this same process.

Look, you're welcome to bring up the proposal again, but if it's a rehash of the old discussion with no new information, I see no way it's going to gain traction.

Doug

3 Likes

For my part, I certainly would not welcome a rejected proposal being brought up again without new information. It’s disrespectful of all participants’ time and effort. There was a thorough discussion and a full review process, and a decision was reached. All the arguments are there as legible as the day they were written, and anyone is free to relive it themselves without taking up the whole community’s time and effort.

5 Likes

Agree — but SE-0132 is just not the case.

Let’s first take a look at the review result:

This original decision note suggested that this proposal requires more design and iteration, and we shall pick it up when the criteria is met.

But what happened then?

This piece of decision note has not even been posted on the Swift Forums. Nor did it indicate that any participants outside of the Core Team was involved in rejecting this proposal. This note raises the concept of “too expensive”, while the “deferred work” of this proposal just consists of lowering that cost.


The story is: the Core Team deferred it on behalf of the community, and then closed it by themselves. And now, the discussion is going to be suppressed because we’re not able to bring it up if there isn’t any “new information”.

What is “new information”?

We’ve been discussing ways to lower that boundary for transition, to fit it into the ABI goal, to narrow its scale or break it into small pieces. Aren’t there any new information? The discussion around the original proposal and its design is not the vital work for revisiting it now. The original review thread has already pointed a clear and positive direction on the detailed design for us, and we can also adopt new ideas from further discussions.

What SE-0132 lacks for today’s review are the additional parts we’d like to discuss and brainstorm: ABI stability, source compatibility, transition, and so on. There’re always calls for revisiting SE-0132, because it solves inconsistency and tries to make Swift Standard Library a good example for Swift API design and evolution. Unless we can fit the existing Collection API in a table just as clearly as the one in SE-0132, there will continue to be confusion about the API naming, and SE-0132 preserves its value till then.

6 Likes

What is your gut feeling about unifying pattern matching and optional unwrapping? Namely, deprecating if let foo = foo and requiring it to be if let foo? = foo, and thus opening up the possibility of removing the case from case let so you can do things like if let .success(foo) = barResult.

2 Likes

We still have to support case:

if case .foo(let a, var b) = bar { … }

Using pattern matching for if let / guard let is actually in the commonly rejected proposals list.

Use pattern-matching in if let instead of optional-unwrapping: We actually tried this and got a lot of negative feedback, for several reasons: (1) Most developers don't think about things in "pattern matching" terms, they think about "destructuring". (2) The vastly most common use case for if let is actually for optional matching, and this change made the common case more awkward. (3) This change increases the learning curve of Swift, changing pattern matching from being a concept that can be learned late to something that must be confronted early. (4) The current design of if case unifies "pattern matching" around the case keyword. (5) If a developer unfamiliar with if case runs into one in some code, they can successfully search for it in a search engine or Stack Overflow.

6 Likes

i'd also add a fix for CharacterSet: rename to what it actually is "UnicodeScalarSet", and add a temporary alias "typealias CharacterSet = UnicodeScalarSet" along with deprecation warnings prompting users to fix the name. later on if needed we can reuse CharacterSet name for some proper Set<Character>.

7 Likes

Could this be an opportunity to revisit Support use of an optional label for the first trailing closure and the APIs which would benefit from it ?

13 Likes