Hi Douglas, first of all I'd like to thank you for this really interesting pitch. I read your proposal and the comments before my post and here is my feedback. I really like the vision of opaque
types presented in the proposal, especially the part which adds more flexibility to work with generics. However I have also a few concerns - most syntax-wise though.
To begin with I noticed that this feature overlaps with some parts of the existential world quite heavily, especially when there is a where
clause applied. From my perspective this whole idea can be treated as a constraint on existentials in general. The only thing that I could observe in your proposal is that an opaque type is constrained to be of the same type and the dynamic type is not exposed anymore, compared to an existential. Since I'm no compiler developer, I'm in no position to judge the type system, but this is my vision as a daily Swift developer.
// Assuming that `P` and `R` are existentials with possible
// associated types iff any of them is a protocol or a class
// The following function can return any type that conforms
// to the required protocol or is a subclass of a required
// superclass constraint
func foo() -> P
func bar() -> P & Q
// In the future we should be able to further constrain
// existentials with a `where` clause, which also does
// not prevent us from combining them even further.
typealias MyP = P where AssociatedType == Int
typealias MyPQ = MyP & Q
// Now I see the `opaque` keyword as a possible constraint
// on existentials.
typealias OpaqueMyP = opaque MyP
// These functions can only return the same type that satisfies
// the requirement.
func baz() -> opaque P
func buz() -> OpaqueMyP
func bum() -> opaque MyPQ
Furthermore I'm not a fan of the proposed _.AssociatedType
syntax nor do I like the dozens of where
clauses as shown in the original post. Instead I would like you to consider a slightly different approach. David (cc @hartbit) and I were talking about the where
clause on existentials, considering the previous work from Austin Zheng, and came to the conclusion that when introducing this feature it should be restricted to be declared on a typealias
only. In short: If you want to create an existential that is constrained with a where
clause you have use a typealias
for that matter. This removes a little of the flexibility but we think that the tradeoff is worth it because:
- it makes you reason more about your type names
- it reduces possible ambiguity of multiple
where
clauses in a single delcaration - it forces the developer to a re-usable approach
- it removes the need of prefix dot
.AssociatedType
or even the_.AssociatedType
of this proposal (which is consistent toprotocol MySequence : Sequence where Element == Int { ... }
syntax) var something: GoodTypeName
is preferred overvar something: A & B & C where AssociatedType == SomethingElse
In that sense I think we should force the declaration of a constrained existential and an opaque
type into the typealias
.
// Instead of this:
var strings: opaque MutableCollection where _.Element == String = ["hello", "world"]
// We would have this
typealias OpaqueMutableStringCollection = opaque MutableCollection where Element == String
var strings: OpaqueMutableStringCollection = ["hello", "world"]
I mentioned before that this pitch feels to me like a constraint over existentials, which raises another question: Why can we introduce opaque
types with a where
clause before we even have existentials with a where
clause?
I have re-written a few of your examples by using a few other missing features to see how I'd prefer the syntax to look like visually and declaratively compared to the original pitch.
extension BidirectionalCollection {
// In this example the returned type cannot be a `RandomAccessCollection`
public func reversed() -> OpaqueBidirectionalCollection<Element> { ... }
}
// This existential would potentially eliminate the current struct in the sdlib
typealias AnyBidirectionalCollection<T> = BidirectionalCollection where Element == T
// Creating a new `Opaque*` family of types similar to `Any*` family
typealias OpaqueBidirectionalCollection<T> = opaque AnyBidirectionalCollection<T>
// Missing feature: extending existentials
extension AnyBidirectionalCollection where Element == String {
public func joined(separator: String) -> String
}
In case of ambiguity of the where
clause I had a discussion a while ago where I pitched a different keyword for constraining conditionally a type directly from the declaration.
extension BidirectionalCollection {
public func reversed() -> OpaqueBidirectionalCollection<Element>
constraints
OpaqueBidirectionalCollection : RandomAccessCollection where Self : RandomAccessCollection,
OpaqueBidirectionalCollection : MutableCollection where Self : MutableCollection {
return ReversedCollection<Self>(...)
}
}
// If there are no other generic parameters to constrain,
// we could omit the reference to the type and just write
extension BidirectionalCollection {
public func reversed() -> OpaqueBidirectionalCollection<Element>
constraints
RandomAccessCollection where Self : RandomAccessCollection,
MutableCollection where Self : MutableCollection {
return ReversedCollection<Self>(...)
}
}
One thing that we have to consider is that we lose the ability to conditionally conform the currently exposed types or the returned opaque
type to custom protocols. There is no way to inject any further constraints into the above reverse
function. It would be a major breaking change and removing some of the current flexibilities until we can extend opaque
types ourselves.
extension ReversedCollection : MyProtocol where C : MyProtocol { ... }
// This is a required alternative that must exist!
extension opaque BidirectionalCollection : MyProtocol where C : MyProtocol { ... }
How about something like this?
extension Int : P {}
struct T : R {
// Uses the same extra keyword `constraints` to avoid ambiguity
// with possible `where` clause
typealias OpaqueP = opaque P constraints OpaqueP == Int
func someValue() -> OpaqueP { ... }
func someOtherValue() -> OpaqueP { ... }
}
To sum up I would like to see the evolution in the following order:
where
clause for existentials (andopaque
types later) allowed only on atypealias
- adding
opaque
types as a constraint over existentials - adding a
constraints
keyword (or similar) to not create confusion or ambiguity with thewhere
clause - allow extending existentials and
opaque
types
There are still some questions left:
-
Why is is it not possible to have
opaque
constants? A result of an query function that returns anopaque
type does not need to be always mutable, what do I miss here? -
Can we combine multiple
opaque
types?
typealias OpaqueP = opaque P
typealias OpaqueQ = opaque Q
// Is this possible?
typealias OpaquePQ = OpaqueP & OpaqueQ // means `opaque P & Q`