[Discussion] Eliding `some` in Swift 6

I agree that any Collection is not the best example for existential extensions. Equatable or Hashable is more useful to think about.

So, let's hypothetically consider the reasons for existential extensions.

Currently, AnyHashable fills the job as a type eraser of a value that's known to be Hashable. Not only this, but AnyHashable is, itself, a Hashable entity.

The way this has been accomplished from the inception of the language is through a type erasing wrapper. Something that looks like this:

struct AnyHashable: Hashable {
    init<T: Hashable>(_ value: T) {
        [...]
    }
}

Additionally, AnyHashable.init is rarely called because Swift implicitly converts Hashable values into AnyHashable containers.

So, seemingly, type-erasers should ideally follow these two patterns:

  1. A type-eraser should conform to its erased type's protocol (aka self-conformance).
  2. A type-eraser should allow implicit conversion from its erased type to itself.

With Swift 5.7, the existence of any Hashable seemingly supersedes AnyHashable seeing that:

  1. An existential can conform to their own protocols (e.g. any Error conforms to protocol Error).
  2. An existential does allow implicit conversion from its conforming type to itself.

That's why I believe there's value in keeping the AnyXYZ form as a way to indicate self-conformance. The only difference is that the required syntax would be elegantly reduced to just this:

typealias AnyHashable = any Hashable

extension AnyHashable: Hashable {
    [...]
}

The pattern above guarantees at a glance the fact that AnyXYZ:

  1. Self-conforms.
  2. Allows implicit conversions.
  3. Is always equivalent to any XYZ

There's a difference between AnyHashable and any Hashable:

struct Apple: Hashable {}
struct Orange: Hashable {}

let fruits1: [AnyHashable] = [Apple(), Orange()]
let fruits2: [any Hashable] = [Apple(), Orange()]

func test() {
    _ = fruits1[0] == fruits1[1] // ok
    _ = fruits2[0] == fruits2[1] // Error: error: binary operator '==' cannot be applied to two 'any Hashable' operands
}
2 Likes

+1

I personally think that public methods should still require any and some

Yes, currently. My point is to make them no different from one another, by enabling existential extensions.

2 Likes

It’s already confusing enough. I’ve already learned enough things by doing what I think is right and having the compiler tell me it was wrong and how to fix it to agree with the “let the compiler guess” plan.

1 Like

"guess" sounds like "heuristic" above. Again, I haven't been able to imagine an example with any ambiguity. I'm sure you're both thinking of something I'm not—please demonstrate!

I definitely do not have an example. Until Becca explained it with chickens and boxes at WWDC I didn’t even really understand the concepts. I just put some when SwiftUI told me to and avoided using existentials.

My preference would be to keep some P so that bare P is unambiguously for use in conformance clauses, but I may be swayed to prefer eliding (and simultaneously with any’s introduction) if Ben is right about the change from default-existential to default-opaque in existing code being nearly seamless.

1 Like

No, if we were to elide some in Swift 6, you would still be able to use a bare protocol name as a conformance constraint without any special new syntax. It's no different than what happens today when you write a bare P in a type context to implicitly mean any P, but <T: P> and <T> where T: P still does the right thing. Type resolution knows whether it's in type context or conformance constraint context, and can resolve to either a bare protocol or an existential/opaque type accordingly.

Personally I would like to stick to explicit some and any. It seems to me that we would take a step back if we would allow 3 possible declarations where two of them would mean the same but even worse after Swift 6 opposite to current status quo.

BTW could be possible to write:

protocol P {
  func make() -> some View
}

?

5 Likes

In the context of a function or type definition, the last bullet point could only be used as a constraint, that is, after a colon. Another use of the "plain Foo" would be in the protocol definition itself, that it, protocol Foo, and in extensions to it, that is, extension Foo: in any other case you would either use any for declaring an existential (that is, var items: [any Item] = []) or a concrete generic type with some.

I must say I think this is a terrible idea, and very hard to motivate.

We just added any to make it clear that P means different things in where X: P and let x: P = X(). We added three letters but gained a lot of clarity. We also gained a nice parallellism between some P and any P. It's easy to see, easy to use, easy to explain.

Very good, three extra letters are a small price to pay. But now it is suggested that we should remove exactly the same amount of clarity, just to save four letters, that seems very ill advised.

Swift explicitly has a policy of not sacrificing clarity for brevity, isn't that exactly what this does? The only gain is four fewer letters.

The fact that this will make some existing code that implicitly uses any suddenly implicitly use some doesn't seem like a good argument. To me it seems more like a counter argument.

16 Likes

Isn't this exactly the situation we wanted to get away from, where the same syntax is used for two different things? If that was a good argument for introducing any, it's a good argument for not eliding some.

4 Likes

There are only two concepts here - placeholders and existentials. And the former should be used vastly more frequently than the latter. It makes entire sense to me to make the common case bare and the rarer case explicit.

I’m also not entirely convinced that allowing time with the ‘some’ keyword will serve the programmer education goal in a meaningful way. It seems more likely to me that programmers will sprinkle ‘some’ in their code to get it to compile without seeking or needing much in the way of a deeper understanding of the difference between placeholders and existentials.

My inclination would be that if “bare protocol = placeholder” is the end goal - which it should be - we just rip off the bandaid and deal with transition confusion now.

1 Like

Is this a correct analysis? I think extension Collection also extends any Collection except conformance.

protocol P{}
extension P {
    var zero: Int { 0 }
}
func use(someP: some P, anyP: any P) {
    print(someP.zero)  // works
    print(anyP.zero)   // also works
}

No there are three concepts: placeholder, existential and conformance. The idea here is to start spelling the first and last of these in the same way. Up until now the last two were spelled the same, but this was deemed confusing.

6 Likes

In my mental concept map conformance and placeholder are deeply conceptually related. A conformance is describing the properties of the type you you’re defining. A placeholder is describing the properties of the type in a given position.

Conformance is saying “this type is a member of this protocol”.

Placeholders are saying “this parameter is a member of this protocol”.

Existentials are the odd one out here.

I do not quite understand you... This works for me in Swift 5.7 and earlier versions:

protocol P {
  func foo()
}

var ps: [P] = []

Here the protocol is used as a type, any is not required and this usage is unrelated to generics.

Well, conformance and placeholders are dealing with the same situation, a type that conforms to a protocol. But they are used for saying different things about that situation. I mean we care enough about the difference to have a specific notation for "X conforms to P", namely the ":". We could just say `X == some P".

1 Like

Is there a different term we could use that doesn’t conflict with

The term for some P is an opaque type of P. Is that what you mean?