[Discussion] Eliding `some` in Swift 6

This is the kind of example where developer would need to understand how Swift actually works, in order to make sense of the error message and find another way to implement what developer wants.

With elided some this would be a disaster; developers would be forced to be ignorant, and would assume BinaryInteger or any other protocol is just like type, and expect it to behave the same as regular type. Also without the vocabulary of "some", it's back to cryptic error messages that make no sense. Developers without knowledge of what is actually happening will feel like this is a dead end.

On the other hand "some BinaryInteger" will at least give a hint that this is not a regular type and thus cannot always be expected to behave like regular type. And having "some" in the vocabulary makes it much easier for the error message to explain it, and also makes it easier to google what other people have done to get their things done.

Now, you could also create all kinds of rules where in some places "some" is elided, and some places it's still required, but from the perspective of novice developer those kind of rules are arbitrary, and would again result in a whack-a-mole, because that elision effectively prevents forming a coherent understanding of what is actually happening.

10 Likes

I think this would need generic local variables (which I think were mentioned in the 'Improving the UI of Generics' post):

let <T: P> f: (T) -> () = takesBareP

If it's possible to handle these sorts of cases where the fact that a function is implicitly generic doesn't matter, I think eliding some would be a lot easier to justify.


This was very illuminating for me. I think if it were possible to write

extension some Collection { ... }

and have extension Collection { ... } be a 'default elision', this would have made the behaviour of protocol extensions more understandable to me - including why trying to do extension ProtocolA : ProtocolB isn't doing what it looks like on the surface.


In some ways protocols seem like they are quantum 'cloud like' types. If you try and narrow them down to a single type (position) using any, you lose some of their behaviour (speed). If you want to retain all their behaviour (speed) using some, you can't know their exact type (position).

4 Likes

I think this is a very compelling example of why we should not elide some: it's useful, it gives extra information, it's a "hint" that the developer must take into account some characteristic of the language that might be overlooked if we only spell the plain protocol.

3 Likes

Not wanting to hog the thread, but one last thought. I think there's a fundamental difference in how we see some. For the supporters of the pitch, it is like let/var, a binary marker that is sort of redundant, and where it would make sense to default to something. I mean we could just decide that

x = 5

means that x is an immutable variable (ignoring local scope overriding etc), and if you want to make it mutable you have to use var.

But for some the situation is different. While x = 5 clearly must at least mean that we're setting x to 5, for me it's not at all clear that func makeP() -> P should mean either func makeP() -> some P or func makeP() -> any P, the most natural interpretation might be that it returns a programmatically constructed protocol object. Or that it's meaningless. Making P mean some P here is to me a very active choice, especially since some P is already sugar for T where T: P.

Finally, just want to throw out an idea: If we keep some, wouldn't it make sense to expand this simplified generics syntax by letting people write things like:

func grabFirst(collection: some T: Collection) -> T.Element?

or

func grabFirst(collection: some Collection T) -> T.Element?

for

func grabFirst<T: Collection>(collection: T) -> T.Element?

It's not shorter, but it's definitely easier to read. The only downside is that we're not explicitly declaring that T is a generic parameter. Not that we necessarily have to.

From my pov there are three different kingdoms: "regular", generics and existentials, and the easier it is to see the differences between them - the better. I won't be happy with the language that hides this relevant difference from me and "helps" to write either "accidentally generic" and/or "accidentally existential" code. Hence I would be against the pitch form of this proposal if it's pitched.

5 Likes

I agree. Changing from a bare protocol to using some is not merely making a minor boilerplate change to appease the compiler. It makes an important difference to the meaning of the code, which the developer (and others working on the codebase with them, or using the APIs they vend) should be able to observe. I don't think the argument that some is closer in meaning to the bare protocol than any is to the bare protocol is strong enough to justify routinely eliding some. They may be closer in meaning, but that is not a reason for them to have the same syntax.

I also agree with the other comments about protocol extensions. The fact that extension P has the unclear behaviour that it does shouldn't be an argument for doing it here too.

Seeing recent code examples with explicit use of some and any everywhere where they apply is one of the few times in my programming career where I've caught myself thinking "wow, I really like this language design choice". I would be sad to see it go away. Consistently providing the vocabulary to reason about use of some P or any P feels much more useful, and safer, than the alternative.

I had similar thoughts, and that's exactly why I think it would be a huge mistake to elide some. When I started working with SwiftUI and coming across some View, that was the only place I could recall seeing that construct, so I just used it without really understanding what the code was doing. I didn't really understand the SwiftUI-related discussions of type erasure either. But with only a few minutes learning about some and any from WWDC, that confusion is totally gone. Hiding this complexity out of concern that newcomers may misunderstand may accomplish the exact opposite of your goal. Developers will be hindered in coming to a full understanding of the code they write.

Tricking people into writing generic code without realising what they're doing doesn't seem like a useful goal. I am not necessarily fundamentally opposed to eliding some, but I remain unconvinced that the reasoning outlined here justifies it, or that it would not cause more problems than it solves.

13 Likes

Even if there were a major version separating these things, would this concern be eliminated? What if someone comes along in 2 years and bumps their library by two major Swift versions at once?

I feel that implicit some will make this code harder to understand why it's invalid:

protocol A {}

protocol P {
	associatedtype T: A
	func value(for input: A) -> T
}

struct AWrapper<Content: A>: A {
	let content: Content
}

struct S: P {
	func value(for input: A) -> A {
		AWrapper(content: input)
	}
}

The current error (when using explicit some) is:

<source>:12:8: error: type 'S' does not conform to protocol 'P'
struct S: P {
       ^
<source>:4:20: note: protocol requires nested type 'T'; do you want to add it?
    associatedtype T: A

This may be solvable by having better error messages (e.g. a note here saying that the return value of value(for:) cannot satisfy P.T because it depends on on A), but we should be careful to consider if we're making other developer mistakes harder to understand.

(I forget my exact use-case when I ran into this, but I do remember that A was SwiftUI.View)

1 Like

The case of primary associated types is interesting one. Angle brackets mean equality, not conformance, so for an Array<Int> it’s where Array.Element == Int.

With Collection<Int> it’s the same: where Collection.Element == Int.

However with Collection<some BinaryInteger>, while technically equality, it’s effectively conformance to BinaryInteger. So it’s effectively where Collection.Element: BinaryInteger, not where Collection.Element == BinaryInteger. I do find the ability to use some in primary associated types very powerful as conformance is much more useful than equality.

Syntax of Collection<BinaryInteger> leads to thinking of ” == BinaryInteger”, which is incorrect. This is the same thing as with current protocol extension syntax; the elided syntax confuses developers and prevents understanding of what really is going on.

7 Likes

Combining my experience in other languages with my how some T has been explained as an alternative form of generics, I would at first expect Collection<some BinaryInteger> to be covariant—that is (re)assignable from any value that satisfies the constraint Collection where Element: BinaryInteger. But Swift has invariant type parameters, so I would instead expect Collection<some BinaryInteger> to be a compile error.

This is my exact feeling. I prefer explicit. It took me long enough to understand what “any” and “some” even solve, and now that I do, going back to not requiring one seems like a headache of a journey.

The reasoning for having “any” and “some” involved months of esoteric debate that few of us could even repeat on the spot.

I prefer Swift stay explicit.

We could elide ‘try’ and ‘await’. The compiler knows it is calling a try and await function, we don’t elide there because it is precise. It’s notation for the user not the compiler.

5 Likes

I had a very basic look at gathering some statistics on how possible eliding to some Protocol might be using an instrumented version of the compiler to log argument types and their location. The test data was the top 1000 packages in the Swift Package Index ordered by github stars.

I didn’t get very far.

The original plan was to use the instrumented build output to insert some into the actual sources with a script in a naive way and try to recompile. Straight away I ran up against two problems. The first problem was it is important not to elide to some, something that satisfies a system protocol such as Encodable or Decodable which is expressed as an any. The second problem was that after eliding a signature to some Protocol it was no longer possible to pass in an existential without being able to "open” it. This doesn’t seem to have made the cut with the Swift 5.7 shipped with the Xcode 14 beta but is likely to be an important factor in the viability of eliding to some in terms of compatibility.

One side effect of this fruitless exercise was some concrete numbers on just how source breaking no longer accepting just Protocol would be. Out of 1000 projects only 28 made no use of a protocol as an argument. Adding the other projects up, the estimate is that someone needs to make 41,140 individual edits to the other 97% of projects to have them build in the currently envisioned Swift 6 raw data here. The column anys: is the number of times an existential is currently used in the project. opens: is a very approximate count of where an existential is passed to a now some Protocol argument.

So, some sort of eliding might be advised. For those who reject the idea of eliding remember that Swift has effectively elided to any for function arguments all since day 1. The disruption is caused by eventually taking this option away in Swift 6 as approved with the passage of SE0335. You could view this pitch as a rearguard action to try to remediate some of the proposal's the source invalidating effects by eliding it to the nearly equivalent/preferable some Protocol now possible in Swift 5.7 (Perhaps this was the plan all along?) I now have the impression this might be quite tricky to implement.

So, let’s take a step back and ask: Why are we doing this? The enthusiasm surrounding the review of SE0335 suggests, along with other key improvements, opening existentials, opaque types as function arguments and easing Self constraints, there has been a significant overhaul of the generics system in Swift 5.7. I can’t speak to this but I suspect the notable disruption deprecating just Protocol may tend be sold to developers on the alleged performance cost of passing around existentials. Much is made of this in the proposal. A little benchmarking using this script reveals existentials are indeed about 20% slower but the thing to note is just how fast either form of dispatch is in Swift. An arm processor is able to make billions of function invocations a second so clearly this is not a real world consideration.

So where is the ongoing "active harm” of leaving things the way they are for now (i.e. eliding to any)? If it’s important to them, a developer will still be able to opt into the new opaque/some treatment of protocol arguments, explicitly annotate code with some/any if they wish and take advantage of all the other generics improvements. They just wouldn’t be forced to.

I appreciate there is a higher theme in play that I'm not party to where some/any Protocol are just “types” which no doubt opens the possibility of exciting new generalisations. I regret however the trend seems to be to relegate the simple concept of a protocol, widely understood by all levels of developer, in favour of what appears to be an over abstraction. For example, the error message when you are unable to open an existential has changed from protocol 'P' as a type cannot conform to the protocol itself to the rather terse: type 'any P' cannot conform to ‘P’in Swift 5.7. No mention that P is in fact a protocol which doesn’t give the novice developer much to go on. Couldn't the message be at least: protocol type 'any P' cannot conform to ‘P’? Or, better still is there no way to remove this limitation? If any P can’t conform to P what can?

1 Like

This has been a constant source of confusion, and probably the main reason for introducing any. For what it's worth, removing that elision has already been decided, separately from this pitch.

The other major source of confusion around protocols, at least for me, but seemingly for many others, is extension P, which is essentially already eliding some. So in a way we have already tested out what is proposed here, we should run a poll to see how many developers have been confused by extension P. I certainly have, and I have been using Swift daily since it was released.

1 Like

My tiny contribution to this forum will start from this important paragraph:

Indeed, if we suppose that a bare P is an elided some, then we would end up with:

func f(_ x: P)  // means f(_ x: some P)
func g(_ x: P?) // means g(_ x: (any P)?)

As Holly mentioned, only optional existentials accept raw nil literal at the point of use:

g(nil)

Which means that we can not know if func g(_ x: P?) could mean g(_ x: (some P)?) without looking for all uses of g, just in case we would find a nil literal.

We have a similar problem with empty collection literals [] and [:] which can not be used as explicit arguments unless the function accepts a collection of any P:

func f(_ x: some Collection<some P>) { ... }
h([]) // error

In both cases, optionals and collections, the language designer does not know the intent of the api designer. Is it the intent of the api designer that a function can be called with untyped arguments such as nil, [], [nil], [:]?

The cases of optionals and collections are not identical, though, because in the case of collections, the api designer must be explicit:

For example, the api designer may, if needed, provide two overloads. This allows the user to feed an empty array literal. And the api designer can still provide an efficient implementation for homogeneous collections:

func f(_ x: some Collection<some P>) -> String { "generic" }
func f(_ x: some Collection<any P>) -> String { "existential" }

f([])         // "existential"
f([1])        // "generic"

If the api designer wants the user to be able to put nil literals in the input, he can change the api:

func f(_ x: some Collection<some P>) -> String { "generic" }
func f(_ x: some Collection<(any P)?>) -> String { "existential" }

f([])         // "existential"
f([nil])      // "existential"
f([1])        // "generic"

I do not say that an api designer should write such code. I say an api designer can write such code when deemed necessary, driven by the expected ways the api will be used.

Now, back to our naked P? and some elision.

  • Given we can not know if the intent of the programmer is to allow nil literal as an argument or not,
  • Given we want to foster generics over existentials,

My suggestion is the following:

  1. In non-public apis, generate two overloads (and discard the ones that do not compile, if any):

    // Source code
    func f(_x: P?) { ... }
    
    // Generated
    func f(_x: (some P)?) { ... }
    @_disfavoredOverload
    func f(_x: (any P)?) { ... }
    

    The disfavored overload would only exist to support f(nil). Otherwise, the generic overload would always be preferred.

    EDIT: f(_ x: P? = nil) is interesting as well. Maybe as below?

    // Source code
    func f(_x: P? = nil) { ... }
    
    // Generated
    func f(_x: (some P)?) { ... }
    @_disfavoredOverload
    func f(_x: (any P)? = nil) { ... }
    

    EDIT 2: I'm not sure how this idea would scale with multiple arguments f(_ x: P?, _ y: Q?).

  2. In public apis, forbid naked P and P? (we don't want to export two symbols in case of optionals, because this would go too far).

2 Likes

I do very much agree that it's an ergonomics issue for bare nil not to be usable, but it also doesn't seem workable to have an entire page of workarounds to make it work, and (IMO) it doesn't seem reasonable to explain to users that P? somehow means (any P)? while P means some P.

If we'd built out the intrastructure to make Never a true bottom type (which, in the fullness of time, hopefully we still can), since Optional already magically supports covariance, then bare nil passed as an argument of type (some P)? could denote Optional<Never>.none and the underlying type of some P would be Never.

2 Likes

Can't we just require people to specify which nil they mean, like we already do in places? They probably don't mean the nil from Never? anyway.

let f = [nil as Int?]
let g = [Optional<Int>.none]
let h: [Int?] = [nil]
let i = [nil] // doesn't work, of course
1 Like

We could—i.e., you could dispute the entire premise that it’s important for folks to be able to call such a function with bare nil as an argument.

However, supposing the validity of the premise, how should we go about supporting it? That’s the core of what I’m replying to:

Well, they do…

There’s a whole slew of useful generic algorithms where you may want to do something with a value of type (some P)? when it’s non-nil, but where all you need to know about a nil value is that it’s nil, and Optional<Never>.none provides exactly what you need.

Indeed, Optional<Never> can only be nil, so to my mind they are related in the same way that the Void type is related to the value ().

So, if a user doesn’t need or want to specify the underlying type, and the author doesn’t need or want to force the user to do so, then what else could they mean?

1 Like

Arguably, encode(to encoder: Encoder) and init(from decoder: Decoder) should really have been encode<E: Encoder>(to encoder: E) and init<D: Decoder>(from decoder: D) all along (i.e. they should have been some, not any). So as long as we change the definitions of Encodable/Decodable too, changing the meaning of the naked protocol would make perfect sense in this case. I’m not sure how tractable that is though, given that they’re ABI.

1 Like

In an ideal world and if this were 2014 I would agree. Looking at the arc of the original proposal and including this pitch it would seem to me the compiler folk would like to change their minds about the default method of passing something conforming to a protocol to a function from any(existentials) to the new some(opaque) types which emerged during the development of SwiftUI. Existentials were always a bit of a strange bird (though they have their qualities in collections) and there are many reasons to move to something some based but the question is whether the wider community is going to sustain the quite dramatic source breaking that results making the change now and buy into the new concepts.

An update to the numbers I provided, many packages explicitly specify the swiftLanguageVersion and they tend to be the ones that use protocols a lot so the actual number of source edits required to make the shift to any/some is 9581 and those could of course simply add the language version to the package (663 of the 1000 I tested) in the short term.

3 Likes

FWIW I find @hborla’s OP very well thought out and the arguments it raises seem very reasonable to me.

So, do we automatically “improve” programmers’ code by changing a default of implicit any to a default of implicit some in many cases, thereby improving performance and usability characteristics of the code? I don’t think we should.

Programmers are still getting used to some, if they’ve used it at all, and any is totally new. So I don’t think we should try to guess what programmers might do next with their generic functions that have been written with the restrictions of any in mind.

What I’d prefer to see in 5.next is a removal of implicit anything via an auto-migrator that simply makes explicit what is currently implicit. That would cause LOC churn, but would be entirely safe, as in, we have no compilation or heuristic issues to worry about. Un-migrated code in 5.next would simply not build without explicit some or any. Users creating new code in this way would be forced via compiler errors to make a decision for one or the other (hopefully guided by helpful docs / fixits). I feel like the use of Any / any is already a “dirty word” / potential code smell for most, which makes removing it (based on manual human discretion) seem like an obvious goal. If that’s not the case then I believe more education is key here (fixits, blog post on swift.org).

In Swift 6 we could start allowing implicit some again. That would not be a source-breaking change and would not cause any LOC churn. It would be akin to having explicit type annotations in the codebase. They wouldn’t need to be there but they also wouldn't harm anything. Users could opt-in to a manual removing of explicit some via a cli tool / migrator.

6 Likes