Thank you for pushing this proposal forward! Aside from the any Protocol
explicit syntax, I'm a bit concerned about the automatic type erasure for associated types in covariant positions:
Covariant Erasure for Associated Types
When invoking a member, Self
-rooted associated types that
- do not have a known implementation and
- appear in covariant position within the type of the member
will be type-erased to their upper bounds as per the generic signature of the existential that is used to access the member.
In Swift, until now, type erasing has been an explicit action made by the user. An heterogeneous array results in an error instead of a warning
let array = [1, "two"]
// error@-1: Heterogeneous collection literal could only be inferred to '[Any]';
// add explicit type annotation if this is intentional
// [fix-it] Insert ' as [Any]'
and you need to explicitly add as [Any]
in order to be able to compile your code. I would like the same treatment for associated types.
As a general rule (and under the assumption of expanding opaque types to be available also in non-return positions and as a tool for expressing unnamed generic parameters), I wouldn't distinguish between covariant and contravariant positions and would indiscriminately replace Self
and associated types with opaque types of their upper bounds. Opaque types are a powerful and easier to understand concept than variance IMHO, since they can be explained as "unknown" types.
protocol P {}
protocol Q {
associatedtype A
associatedtype B: P
var a: A { get set }
var b: B { get set }
func takesA(a: A)
func takesB(b: B)
}
var anyQ: any Q = instanceConformingToQ
anyQ.a is some Any
anyQ.b is some P
anyQ.takesA is (some Any) -> Void // practically uncallable
anyQ.takesB is (some P) -> Void // practically uncallable
Then, if the user's desire is to work with existential types, they must explicitly use as any Q
, in line with what we do for heterogenous collections:
var anyQ: any Q = instanceConformingToQ
var b = anyQ.b as any P // or 'var b: any P = anyQ.b'
Opaque types also egregiously handle mutability and path-dependent types and may allow some of the rejected code to compile in the future. As a future direction, if anyQ
were non-mutating and since protocol existential types are value types, then it could be implicitly handled as an instance of some Q
upon initialization:
let anyQ: any Q = instanceConfomingToQ // equivalent to 'anyQ: some Q'
anyQ.a is (some Q).A // i.e. (type of 'anyQ').A
anyQ.takesA is ((some Q).A) -> Void
anyQ.takesA(a: anyQ.a) // valid today if anyQ is some Q
The example of RandomAccessCollection.dropLast(_:)
would then become:
Proposed solution in this proposal
let collection: any RandomAccessCollection = [1, 2, 3]
// head is any RandomAccessCollection
let head = collection.dropLast()
// error since +(_: any RandomAccessCollection, _: any RandomAccessCollection) is not available
let y = collection + head ❌
Current behavior of opaque types
let collection: some RandomAccessCollection = [1, 2, 3]
// head is (some RandomAccessCollection).SubSequence
// head is (type of 'collection').SubSequence
let head = collection.dropLast()
let y = collection + head ✅
print(y) // prints [1, 2, 1, 2, 3]
With opaque type inferred on non-mutating instances
let collection: any RandomAccessCollection = [1, 2, 3]
// head is (some RandomAccessCollection).SubSequence
// head is (type of 'collection').SubSequence
let head = collection.dropLast()
// no error, collection is immutable, same behavior as above
let y = collection + head ✅
while the example of Sequence.enumerated()
would become:
Proposed solution in this proposal
func printEnumerated(s: any Sequence) {
// error: member 'enumerated' cannot be used on value of type protocol type 'any Sequence'
// because it references 'Self' in invariant position; use a conformance constraint
// instead. [fix-it: printEnumerated(s: any Sequence) -> printEnumerated<S: Sequence>(s: S)]
for (index, element) in s.enumerated() { ❌
if index < 5 { print(element) } ❌
}
}
Current behavior of opaque types
let s: some Sequence = "Swift Evolution"
// s.enumerated() is EnumeratedSequence<some Sequence>
// s.enumerated() is EnumeratedSequence<(type of 's')>
for (index, element) in s.enumerated() { ✅
// index is Int
// element is (some Sequence).Element
// element is (type of 's').Element
if index < 5 { print(element) } ✅
}
With opaque type inferred on non-mutating instances
func printEnumerated(s: any Sequence) {
// s is non-mutating, handled as 'some Sequence', same behavior as above
for (index, element) in s.enumerated() { ✅
if index < 5 { print(element) } ✅
}
}
The any
syntax
Regarding the any
syntax, I agree with the proposal, in the sense that it should be implemented for all existential protocol types regardless of them being PATs or not. I think that it should be introduced as a warning along with this "unlocking" though, and not in a subsequent proposal. Users would be able to type Collection
where a type is expected, their code will compile, but a warning+fixit should appear to automatically add any
in front of it. As suggested by @Chris_Lattner3, it may be enforced and become an error later on.
I also think that having any Any
and some Any
should be avoided and in that regard I would love to see Any
replaced by Type
. Type
as an identifier is already in a gray area, since MyType.Type
cannot refer to a custom type (even though diagnostics may currently suggest otherwise):
struct MyType {
// error@+4: Type member must not be named 'Type', since
// it would conflict with the 'foo.Type' expression
// [fix-it] If this name is unavoidable, use backticks to escape it
// Replace 'struct Type {}' with 'struct `Type` {}'
struct Type {}
}
struct MyType {
struct `Type` { static let foo = 2 }
}
let foo = MyType.Type.foo // error: Type 'MyType.Type' has no member 'foo'
let foo = MyType.`Type`.foo // error: Type 'MyType.Type' has no member 'foo'
I would like Type
to be the empty protocol, i.e. the protocol which all types implicitly conform to, and Any
become a typealias:
typealias Any = any Type
We could even introduce Unknown
as a typealias for some Type
. With this rename in place, Any
would be what it already is, i.e. the existential type of all types and it's usage in code wouldn't change since nobody is really using it as a protocol (unless someone wrote protocol MyAny: Any {}
, which is of dubious use). Therefore, the Swift 6 migrator wouldn't have to replace all the Any
s.