Unlock Existentials for All Protocols
- Proposal: SE-NNNN
- Authors: Anthony Latsis, Filip Sakel, Suyash Srijan
- Implementation: apple/swift#33767
Introduction
Swift currently offers the ability for protocols that meet certain criteria to be used as types. Trying to use an unsupported protocol as a type yields the error [the protocol] can only be used as a generic constraint because it has 'Self' or associated type requirements
. This proposal aims to relax this artificial constraint imposed on such protocols.
Motivation
Currently, any protocol that has non-covariant Self
references or associated type requirements is not allowed to be used as a type. Initially, this restriction reflected technical limitations (as Joe states here); however, such limitations have now been alleviated. As a result, users are left unable to utilize a powerful feature for certain protocols. Thatâs evident in a plethora of projects. For instance, the Standard Library has existential types such as AnyHashable
and AnyCollection
and SwiftUI has AnyView
.
Generics are most often the best mechanism for type-level abstraction, which relies on the compiler knowing type information during compilation. However, type information is not always a gurantee, which is why the value-level abstraction of existential types is extremely useful in some cases.
One such case is heterogenous collections, which require value-level abstraction to store their elements of various types:
protocol Identifiable {
associatedtype ID: Hashable
var id: ID { get }
}
let invalidIdentifiables: [Identifiable] â
// The compiler doesn't currently allow that.
// So, we work around that by creating a
// custom existential type: 'AnyIdentifiable'.
struct AnyIdentifiable {
typealias ID = AnyHashable
var id: ID { ... }
}
let myIdentifiables: [AnyIdentifiable] â
Furthermore, dynamic environments are also known to lack type information. Therefore value-level abstraction can be exploited in cases such as previewing an application, where the application's components are dynamically replaced, in the file system where a file representing an unknown type might be stored, and in server environments, where various types could be exchanged between different computers.
Morover, the availability of an existential type for a given protocol is sometimes quite unintuitive. That is, today, a protocol qualifies for an existential type provided that it lacks any associated type or non-covariant Self
requirements; however, the associated types of a protocol can be fixed via the same-type constraint. As a result, post after post has been created asking for this restriction's removal:
protocol User: Identifiable
where ID == UUID {
var username: Strng { get }
}
let myUsers: [User] â
// This is forbidden today
// for no apparent reason.
All in all, supporting existential types for all protocols is useful for many situations that involve dynamicity. Not to mention that there are many questions by language users asking about this behavior. Taking everything into consideration, we are confident that addressing this abnormality in the language will build a stronger foundation for future additions.
Proposed Solution
The constraint prohibiting the use of some protocols as types will be lifted. Consequently, boilerplate code in many projects â especially libraries and frameworks â will be significantly reduced.
Detailed Design
The compiler will no longer differentiate between protocols that donât have Self
or associated type requirements and those that do. However, some restrictions will apply to the use of requirements referencing associated types as seen in the below examples.
Examples:
Protocol with Self
and Associated Type Requirements
protocol A {
associatedtype A
var a: A { get }
}
struct S: A {
let a: Int = 5
}
let myA: A = S() â
let a: Any = myA.a â
// We donât know what associated type
// 'A' is on the existential type of 'A'.
extension A {
var opaqueA: some Any {
a
}
// Note that it references
// the associated type 'A'.
}
let opaqueA: some Any = myA.opaqueA â
// Opaque result type don't
// type-erase; they just conceal
// the underlying value from the
// user. As a result, the above
// is not allowed.
Protocol with Known Associated Types
protocol A {
associatedtype A
var a: A { get }
}
protocol RefinedA: A
where A == Int {}
struct S: RefinedA {
let a: Int = 5
}
let myRefinedA: RefinedA = S() â
let intA: Int = myRefinedA.a â
// Here we know that the associated
// type 'A' of 'RefinedA' is 'Int'.
Protocol Composition
protocol A {
associatedtype A
var a: A { get }
}
protocol RefinedA: A
where A == Int {}
protocol B {
associatedtype B
var b: B { get }
}
struct S: RefinedA & B {
let a: Int = 5
let b: Int = 5
}
let myRefinedAB: RefinedA & B = S() â
let a: Int = myRefinedAB.a â
let b: some Any = myRefinedAB.b â
// We donât know what type 'B' is
// on the type-erased value 'myRefinedAB'.
Note on conflicting associated types: Consider the following sample:
...
protocol AFixedToInt: A
where A == Int {}
protocol AFixedToBool: A
where A == Bool {}
let composition: AFixedToInt & AFixedToBool â ď¸
// The associated type 'A' has conflicting
// implementations as both 'Int' and 'Bool'
// in this composition type.
The above composition type emits a warning, as providing a value bound to the associated type '
A
' is not possible. It was also disussed to outright emit an error in this case; however, due to source compatibility constraints, the fact that the two protocols' associated types 'A
' are formally distinct and potential for protocol member disambiguation in the future, warnings were chosen.
Source compatibility
This is an additive change with no impact on source compatibility.
Effect on ABI stability
This is an additive change with no impact on ABI stability.
Effect on API resilience
This is an additive change with no impact on API resilience.
Alternatives Considered
We could leave Swift as is. That, however â as discussed in the Motivation section â produces boilerplate code and a lot of confusion for language users.
Future Directions
Separate Existential Types from Protocols
To alleviate confusion between existential types and protocols it has been proposed that when referring to the former some different way be used. Some advocate for the modifier 'any' to serve that purpose: any Foo
, while others propose parameterizing 'Any': Any<Foo>
. Whatever the way of achieving this is, differentiation between the two would be useful as it would â among other reasons â prevent beginners from unknowingly using existential types, which can adversely affect performance.
Introduce Constraints for Existential Types
After introducing existential types for all protocols, constraining them seems like the next logical step. Constraining refers to constraining a protocolâs associated types which will, therefore, only be available to protocols that have unspecified associated types. These constraints would probably be the same-type constraint: where A == B
and the conformance constraint: where A: B
:
typealias A = Any<
Identifiable where .ID == String
>
typealias B = Any<
Identifiable where .ID: Comparable
>
Allow Accessing Associated Types
Currently, accessing associated types through a protocol's existential type is invalid. However, we could ease that constraint by replacing every associated type of the 'base' protocol with its existential type:
protocol Identifiable {
assciatedtype ID: Hashable
var id: ID { get }
}
struct S: Identifiable {
let id: Int = 5
}
let myIdentifiable: Identifiable = S()
let id: Any<Hashable> = myIdentifiable.id â
Make Existential Types Extensible
Today, no protocolâs existential type can conform to the protocol itself (except for @objc
protocols). This is quite unintuitive â as is evident by countless questions asking about it. Such a feature could automatically apply to protocols that lack initializers, static requirements and functions with parameters bound to Self
(as discussed in related post). To handle the cases that do not meet the aforementioned criteria for implicit conformance, the following syntax has been proposed:
extension Any<Hashable>: Hashable {
âŚ
}
Other protocols that do meet these criteria would have existential types that automatically gain conformance to their corresponding protocol. In other words, a type such as Error
would automatically gain support for conformance to the Error
protocol.