I'd state this a bit differently. I think we shouldn't be afraid to introduce a keyword when we are introducing a new kind of "thing". But we should be wary of introducing any new "thing", especially if it's very similar to something that already exists in the language.
I think your Opaque<...> and Any<...> suggestions fit nicely together. I have two concerns. First, the Any<...> was discussed extensively as part of SE-0095 a while back, and at this point I don't think we should go back and change it. Second, Opaque<...> wouldn't be a general-purpose type you could use anywhere: it is syntactically limited to result types, which makes opaque feel (to me) more like a modifier on the result type than a full-fledged type in the type system.
type(of:) returns the underlying concrete type, at run time.
I've been thinking about this for a bit, and the second bullet is really what gets to me. That syntax I conjured up to describe conditional conformance is rather awful. It's completely non obvious and quite verbose. @dabrahamsimproved on it a bit, but it's verbose by necessity.
I think the right course forward for this proposal is to introduce opaque type aliases, which give a name to the opaque type so that it can be reused/tweaked. That provides a natural way to describe the conditional conformance, so we can eliminate the awful conditional-conformance-in-the-result-type syntax from the proposal.
I'll ignore all of off-topic stuff from your post, but I want to respond to this part here. Generic protocols can replace associated types (some things would get worse, some things would get better), don't change much about existentials (the same problems of type identity remain), or generalized existentials (some trivial cases get simpler; the general case remains about the same), and don't solve the problems opaque result types solve (which are primarily about type identity).
I've seen this same line about generic protocols being "all we need" to simplify the language a number of times, enough that I think a significant number of people believe it despite the complete lack of a testable design that would achieve the stated wins. A handful of small examples doesn't cut it---we're talking about something that could radically reshape the way we work with generics, as well as the standard library. If you want to talk design of generic protocols, that's great... for another thread, but please refrain from claiming that "feature X will fix everything" unless you're prepared to provide sufficient detail to evaluate said claim.
The signature you list would allow the caller to choose a type for Opaque, and it could be any Collection where Element == Int. This feature has the callee choose one type; itâs just that its choice is hidden.
You (the caller) actually can't choose a type for obvious reasons. All you can do is use exactly Collection where Element == Int, which will be possible once opened existentials land.
The type that makeIterator returns is not stated and can be a private type (it commonly is in Java/Kotlin/Scala), i.e. it provides opacity.
Surely Java/Kotlin/Scala represent a thoroughly tested and complete design? I just don't buy the argument that Swift is so different from these languages that despite the solution known to work well in these languages it can't possible work in Swift. As further evidence, Scala provides both generics and associated types and the latter is rarely used.
Sure the compiler/runtime in Java/Kotlin/Swift has to work hard to optimise the code, but the compilers for Java/Kotlin are probably quicker than Swift (Scala is probably a similar speed to Swift) and the resultant code from all three executes as quickly, therefore proving that it is possible. But obviously generic protocols with the associated optimisations are significant work to implement.
No, Iâm saying that you can write a function with exactly this signature right now, and the caller can write as Array<Int> or as Set<Int> or whatever else they want right after it, and foo() just has to figure out how to return the type the caller specifies. This proposal has a different behavior, so it needs to have a different syntax.
Could on elaborate a bit on how func foo() -> opaque Collection where _.Element == Int {...} would be different? If the actual type is public, you might just guess it by casting, but if it's private the cast will never succeed.
Itâs different because the compiler will diagnose it as a syntax errorâprobably an unknown type attribute, but Iâm on my phone right now, so I canât test it. Your code will be accepted by the current compiler; Dougâs will he rejected. That means we canât change the meaning of your code without breaking source compatibility.
I understand it isn't valid syntax, I am asking about the difference between the semantics of my signature and the one Doug proposes. In other words, why doesn't a generic return type - the underlying type is hidden, but it can be guessed by casting iff it's accessible - satisfy the concept of an opaque type.
You can count me to the folks that want generic protocols in Swift, but I also want HKT, opaque types, all existential features and even more missing generic features. However I disagree that generic protocols solve the current problem nor every other problem. Every generic feature solves a specific subset of issues. Sure some of them do overlap, sometimes a lot (opaque types vs. existentials), however they still allow different solutions to be found to different problems. My only concern about discussion regarding generic protocols is that if we donât stop hijacking every topic about generic features that the core team will be so annoyed that they reject it alltogether. I donât want that to happen so I encourage everyone to calm down about that particular feature and wait until itâs time to bring it to the table.
Doug means that changing the syntax from ATs to generic protocols doesn't cross out the problems we currently have, the generics manifesto and the roadmap for the type system in general, which is quite different from that of Java and even Kotlin. You're talking about syntax, Doug is about implementation (for Swift).
Thatâs not quite the point of opaque types. Consider if the function returned an opaque Equatable; an existential type doesnât help since you donât know the Self requirement (so the == method needs a dynamic type check as it would in e.g. Java). With an opaque type, you know that the particular type returned from a particular method will always have the same associated types; as such, you can safely use ==, or create an array of that type, or anything else that you'd be prevented from doing without generalised existentials or other generics features.
In general, I'm +1 on this proposal. I do think the syntax is ugly and cumbersome for conditional conformances, but if that's hidden behind typealiases for the user then I think it fits with progressive disclosure, and I can't think of anything better. I certainly think the idea of opaque types is better than leaking out names like LazyMapFilterCollection to the user, and I think opaque types may end up being more readable since they only expose the information about the type that the user is likely to care about â what you can do with it given the protocols it conforms to.
Given func foo<Opaque: Collection>() -> Opaque where Opaque.Element == Int {...}, the caller can indeed choose the return type.
One does this by using the type coercion operator as, which is widely misunderstood:
let x = foo() as [Int]
This does not involve any casting whatsoever because as is not a casting operator here: it merely specifies the return type. The function foo must return a value of whatever type the user so chooses as long as the choice conforms to the stated constraints. That is, the author of the function chooses the constraints and the user chooses the type.
An opaque return type means that the author of the function chooses a single return type not disclosed to the user but guaranteed to conform to the stated constraints. That is, the author of the function chooses both the constraints and the type.
I don't understand. I would think that the callee chooses the type because it calls the constructor creating the result value.
Just to be sure I did verify this (with a generic class instead of a protocol because we do not have existential yet) and got a cast error as expected:
Could not cast value of type '__lldb_expr_21.Hidden<Swift.Int>' (0x114ac1e10) to '__lldb_expr_21.Other<Swift.Int>' (0x114ac4e88).
Thanks Xiaodi, but I am still not convinced there is a difference.
How is this true? The following should be perfectly fine once we fully support opened existentials.
func foo<T: Collection>() -> T where T.Element == Int { return Array<Int>() }
If such a function really had to be able to return, or coerce to, whatever type satisfies the constraints, the choice would indeed be after the user.
Otherwise, it isn't really a choice â rather, it's a guess through coercing, as it would be for a function returning an opaque type. If you want to coerce to some concrete type, that is.