[Pitch] Elide `some` in Swift 6

-1. some and any bring much needed clarity to usage of protocols, and the language is better with them. Elision would only muddy the waters again.

10 Likes

+1 from me. some is almost always what you want and it's nice to remove some boilerplate from the common case.

In addition to what I wrote before, I want to chime in on how hard it was to understand the interaction between protocols, generics and existentials in the beginning (in Swift 1.0, long before some and any).

The distinction between a type that conforms to a protocol, and the protocol itself is pretty subtle for a beginner, I really don't think that hiding it helps anyone, it only makes sense for more experienced developers.

It also took me a very long time to understand what people meant by "existential", precisely because it didn't have any special syntax.

I also want to say that if bare protocol should mean anything, any P seems like a less surprising choice, even if it is less performant (which probably doesn't matter 99% of the time). Existentials behave like normal types, but some P doesn't.

I think that's the core of my opposition to this proposal:

Since x: some MyProtocol behaves so differently from x: MyType, how could it not add to confusion to use the same syntax for both?

It's not just that they are philosophically different, they behave differently.

Consider:

func foo(x: Int) {
    // do something with x
}

// Countable is a protocol
func foo(x: Countable) { // Implicit `some`
    // do something with x
}

So far so good, the beginner doesn't need to understand exactly why the second one works. But when things get just a little bit more complex, the implicit some falls on its face and the beginner is baffled:

func foo(x: Int, y: Int) {
    let z = max(x, y)
}

func foo(x: Countable, y: Countable) { // Implicit `some`
    let z = max(x, y) // Won't work, developer is thoroughly confused
}

If however, instead of just implicitly adding some, the use of bare protocol is an excellent opportunity to show an educational error message, asking them if they meant "an instance of some specific type that conforms to P, or a wrapped instance of any type that conforms to P".

15 Likes

Agreed. -1 from me as well. Explicit is better than implicit, not withstanding the need for a welcome shortcut for the aforementioned nullability issue though.

8 Likes

I'm also slightly -1 on this change as well. I probably wouldn't mind the change as much if there never was a way to write bare protocols before, but in the current situation this change will invalidate lots of example code, documentation and tutorials.

The only real argument that can be made in favour of this pitch is the fact that without it the migration from Swift 5 to Swift 6 would be harder and I really don't think that we should compromise the future of the language to ease one migration a bit. Other than that this change goes against one of Swift's main goals, namely clarity over brevity, as it clearly takes away the clear distinction between some P and any P that is easily googlable and explainable to beginners.

13 Likes

I’d like to re-iterate my support for the authors continuing to investigate this pitch in the face of the prevailing wind. It’s true my experience with Alamofire and Ben’s experience with and NetNewsWire contrasting options ImplicitSome and ExistentialAny that a blanket re-intrepretation of a bare protocol from meaning any to some without qualification may make the problem worse rather than better. I’ve tried to put forward a tempered, hybrid approach but I underestimated the number and vigour of the “the migration pain is the gain” constituency.

I’m not a luddite. I’d be prepared to acquiesce to a certain amount of disruption if I felt we were moving to a better Swift. I don’t find a Swift where a novice programmer is required to make a choice between two genuinely difficult to understand and teach concepts in order to use a protocol a step forward though. This isn’t a dilemma one encounters in Java for example. I feel in that situation the compiler would be able to make as informed a choice. It’s a strange situation, in the course of two years we’ve moved from many of us being blissfully unaware of essentials to finding the possibility the compiler would make the decision to to use one for us beyond the pale. This just goes to show the power of giving something a label. I'm not saying there aren't other more subtle, perfectly valid concerns.

Much of the “bad rap” existentials suffer from is based on considerations that don't bare close scrutiny or are no longer true. While it is true using them to pass a value to a function is not as efficient as the generic approach I doubt this ever made anyone’s program meaningfully slower as function call overhead in Swift is barely quantifiable. It’s also the case that while it was possible to paint yourself into a design corner putting a value into an existential, we can now “open” them and get the value back - another great innovation from the heady time at the end of 2021 when so many aspects of the usability of Swift generics were revisited and improved.

Let’s not bridle at the prospect someone is trying to introduce a elision. Swift has been eliding for years and the real debate should be the extent to which we can move the dial from eliding any to some with Swift 6. We are already at a Goldilocks point where if you feel the compiler has made the wrong choice or it's a critical distinction that must be visible you an add a annotation since Xcode 13.3.

I’ve been experimenting with the compiler in a naive way and I am almost able to get Alamofire to compile unmodified with a very conservative and comprehensible rule that not storage and only procedure arguments of a simple bare protocol type (no containers or closures) are elided to some. This feels like a sweet spot where disruption is minimised and the migration away from the less efficient passing of existentials to functions can move forward and we can save a coupe of registers.

This leaves a couple of thorny problems which it may not be possible to resolve if you try a broader corpus of software. Code breaks if an argument is elided to some where it was intended to satisfy a legacy protocol or is the override of a function of a class that was compiled as any. Given how difficult it would be to avoid these problems, perhaps this would be a acceptable level of source breaking being more the exception than the rule. What gives me pause though are the silent changes in binding priority changing a function’s signature can give rise to described above. Perhaps it might be possible to produce a diagnostic if the compiler is making a different decision than it would have done in the past.

2 Likes

-1 for this change. For two reasons:

  1. Reading the code: as others have shared, removing the keyword some from opaque types adds more confusion for the readers. It will be more difficult to differentiate protocol names from opaque types (which is similar to what has happened previously between protocol names and existentials). P, some P and any P express the semantics well. We shouldn't sacrifice clarity for brevity.
  2. Writing the code: though writing additional some may be cumbersome, we can solve the issue in different ways. For instance, when user is typing some characters like Identifi in Xcode, the auto completion tool can provide both some Identifiable and any Identifiable. We don't necessarilly need to provide the shortcut at the language level.
9 Likes

+1 on this and essentially for the above reason. The amount of code that we can switch from bare P (as any) to bare P (as some) without a problem is a good indication that for most use cases it is a distinction that it is not necessary for all programmers to grok in order to use the language. It’s definitely something we care about in terms of performance, but there’s a ton of code out there that is not meaningfully performance-bound.

So I would frame the question this way:
“If we moved to bare P as some, how much correct code could a new programmer write without needing to understand the distinction between some and any?”

I think the answer is “quite a lot” which is why I’m +1

4 Likes

In this regard John's paragraph is both provocative and inspiring:

It matches an intuition of mine bare P creates quite some churn for properties.

In the case of concrete types, the developer can switch to any P, or make the container type generic - but this won't scale well when there are many such properties:

// BEFORE
struct MyType {
    var myProperty: P
}

// AFTER 1
struct MyType {
    var myProperty: any P
}

// AFTER 2
struct MyType<T: P> {
    var myProperty: T
}

In the case of protocols, the developer can switch to any P, or introduce an associated type:

// BEFORE
protocol MyProtocol {
    var myProperty: P { get }
}

// AFTER 1
protocol MyProtocol {
    var myProperty: any P { get }
}

// AFTER 2
protocol MyProtocol {
    associatedtype PType: P
    var myProperty: PType { get }
}

In those two cases, the implicit some can never help, right? So those are cases where the implicit any has some advantages.

As a plain data point, I have a big commit where I made explicit all anys and somes. (That's where I learned to deeply dislike (any P)?).

There's a frequent pattern in this code base: functions whose argument is a closure that returns an existential:

protocol P { }
func frobnicate(_ body: () -> /* any */ P) { }

In the specific case of my code base, an existential is better, because it lets the user return one type or another: that's better ergonomics. (Actually, it's also frequently func frobnicate(_ body: () -> (any P)?), which accepts a { nil } closure.)

But whether this existential is a conscious choice or not is not really the question. The hard fact is that this pitch would introduce a breaking change in this method - and the error may only appear in another module:

extension Int: P { }
extension String: P { }

import FrobnicateKit

frobnicate { 1 }     // OK
frobnicate { "foo" } // OK
frobnicate {         // Stops compiling when bare P starts meaning some P
  if Bool.random() {
    return 1
  } else {
    return "foo"
  }
}

This means that the author of the initial method may not see the breaking change - unless their own code base or tests makes use of existential-specific features, such as returning different types from the closure, or returning a nil literal (in the case of closures accepting an optional result).

7 Likes

I think that is a little bit of a straw man argument. It isn't the distinction between some and any that concerns me but the distinction between protocols and types. Protocols are not types in Swift and the use of some and any in places where they might be conflated with types accidentally is an improvement IMO.

9 Likes

Couldn't Xcode highlight protocols with a different colour to make them stand out?

2 Likes

Xcode is not the only environment that Swift developers use. Especially if the distinction is semantic and requires type checker's output, it can't be made without more advanced tools like SourceKit-LSP. Even then, that leaves out editors without LSP support, and also syntax highlighters on blogs, forums, StackOverflow etc.

10 Likes

Protocols are not types, but some P and any P are, aren't they? Are the terms "opaque type" and "existential type" misnomers?

Some P is not a type, it’s a shorthand for making the function etc generic, without having to name the generic parameter. That parameter is the type.

2 Likes

protocols can be referenced through typealiases, which have "typealias" token classification, not "protocol" token classification.

see the table in this bug for an overview of now semantic highlighting behaves today.

like @Max_Desiatov said, semantic highlighters aren't available everywhere, but even if they were, they wouldn't be sufficient to distinguish generics from concrete types.

3 Likes

Leaving aside questions about whether we want to design the language around people who don't use Xcode.. I have another data point. Seems like GitHub - groue/GRDB.swift: A toolkit for SQLite databases, with a focus on application development is another package it is worth focusing on. Sorry Gwendal, I've visually inspected your big diff and very nearly all of the 900 edits you had to make would have been in line with the eliding that would have been applied automatically by the simple rule I put forward above.

There were exceptions notably the problem where a function is intended to provide a conformance to a system protocol eg Decodable, Encodable etc (3 cases) and it seems you can't use some types for constructor arguments in the toolchain. Was there a reason for that?

On the problem of not eliding an argument of a function satisfying a conformance I'm more hopeful that could be realised as the Protocol being elided is likely to come from the same framework that declared the protocol the method is supposed to conform to. So, it could easily be detected under which convention it it was compiled. These are generally system frameworks anyway which I assume aren't going to migrate into the some world as that would create some spectacular abi issues. If you don't understand what I'm talking about I mean methods such as this:

         struct Value : Encodable, DatabaseValueConvertible {
           func encode(to encoder: Encoder) throws {
                var container = encoder.singleValueContainer()
                try container.encode(string)
            }

i.e. as the compiler will know the module Encoder was compiled with any elided to any it will know Encodable is expecting an any. You never want to elide Encoder to some Encoder in this situation. You pretty much never want to elide anything other than a function argument to some.

Considering the amount of time and effort I've spent over the last years trying to convince people that Swift can in fact be used for other things besides iOS programming, it makes me sad to hear this sentence.
If you visit https://swift.org right now, the first thing you read is the following:

So yes, Swift aims to be a multiplatform programming language, and yes, that means we have to design the language with people in mind that don't use Xcode.

Besides, even if you use Swift solely for the purpose of iOS programming you still have to look at code on GitHub, Stack Overflow, or so. We have to design the language for readability/understandability in these contexts as well.

13 Likes

Regarding the ongoing discussion about having a bare protocol mean some in most cases but any in some special cases:

I understand that this could ease the burden of migration (in some cases even tremendously so), but it leaves the language in such a horrible state when it comes to understandability. How am I supposed to teach a beginner that there exists some P and any P, but also P which could mean either one depending on the context?

I have already expressed that I don't particularly like having the bare protocol type at all, but if we absolutely must have it, then please at least make it consistently mean one thing.

11 Likes

I apologise for that throw-away comment. I myself provided a toolchain for Android for a while and have high hopes for Swift on the server if we can solve this problem. I’m going to try really hard to make this my last post on this topic as I’m sure practically no-one can be under any illusions on my stance.

I’m not quite saying that. I’ve a evaluated few things and the most effective rule is elide to some only in some specific cases and any for everything else. If this pitch is to "just elide to some" qualitatively that seems to almost make migration disruption worse not better and it defeats itself.

Looking again at Gwedall’s big diff where he conscientiously went through and tried to do he right thing. The new explicit annotations are almost completly divided between some for arguments (407) and any for everything else (293) which I believe is easy enough to understand and elide. It’s handy this coincides with the engineering win of finally setting right the historical accident that existentials where ever used by default for passing things around. I do feel we should elide to something though if only for the library maintainers as otherwise we are going to create a “language schism” where the net effect of Swift 6 coming out may be library maintainers adding an explicit language version of 5 to their packages and dooming those supporting the compiler to supporting language version 5 for quite some time.

Anyway, I’m going to leave this debate with a pointer to my first naive thoughts a couple of years ago. I find it ironic that my last thoughts align so closely with my initial gut intuition.

If I’ve learned anything in IT (and that's always been subject to debate :grinning:) it's that the only allies you have in one's own personal "rage against the machine” as a programmer are simplicity and stability. SE-0335 always went against both those tenants for me.

Edit:
I don't want this last comment to sound like a glib manifesto against trying to improve things or change in general. It's just sometimes you need to take smaller steps so everybody gets to keep up. Selecting a more pragmatic form of eliding would seem to be the appropriate next step which is why I support this pitch. It will take some of the sting out of the changes to come.

1 Like