[swift-evolution] [Manifesto] Completing Generics

Douglas Gregor dgregor at apple.com
Wed Mar 2 22:33:26 CST 2016


> On Mar 2, 2016, at 5:50 PM, Joe Groff <jgroff at apple.com> wrote:
> 
> 
>> On Mar 2, 2016, at 5:22 PM, Douglas Gregor via swift-evolution <swift-evolution at swift.org <mailto:swift-evolution at swift.org>> wrote:
>> 
>> *Generalized existentials
>> 
>> The restrictions on existential types came from an implementation limitation, but it is reasonable to allow a value of protocol type even when the protocol has Self constraints or associated types. For example, consider IteratorProtocol again and how it could be used as an existential:
>> 
>> protocol IteratorProtocol {
>>   associatedtype Element
>>   mutating func next() -> Element?
>> }
>> 
>> let it: IteratorProtocol = …
>> it.next()   // if this is permitted, it could return an “Any?”, i.e., the existential that wraps the actual element
>> 
>> Additionally, it is reasonable to want to constrain the associated types of an existential, e.g., “a Sequence whose element type is String” could be expressed by putting a where clause into “protocol<…>” or “Any<…>” (per “Renaming protocol<…> to Any<…>”):
>> 
>> let strings: Any<Sequence where .Iterator.Element == String> = [“a”, “b”, “c”]
>> 
>> The leading “.” indicates that we’re talking about the dynamic type, i.e., the “Self” type that’s conforming to the Sequence protocol. There’s no reason why we cannot support arbitrary “where” clauses within the “Any<…>”. This very-general syntax is a bit unwieldy, but common cases can easily be wrapped up in a generic typealias (see the section “Generic typealiases” above):
>> 
>> typealias AnySequence<Element> = Any<Sequence where .Iterator.Element == Element>
>> let strings: AnySequence<String> = [“a”, “b”, “c”]
> 
> Something else to consider: Maybe we should require Any<...> to refer to all existential types, including single-protocol existentials (so you'd have to say var x: Any<Drawable> instead of var x: Drawable). Between static method requirements, init requirements, and contravariant self and associated type constraints, there are a lot of ways our protocols can diverge in their capabilities as constraints and dynamic types. And with resilience, *no* public protocol type can be assumed to resilient implicitly conform to its protocol, since new versions may introduce new requirements that break the self-conformance. If protocols are namespaced separately from types, you could still do something like:
> 
> typealias Drawable: Drawable = Any<Drawable>
> 
> if you intend to use the protocol type primarily as a dynamic type (and assert that it's self-conforming).

Yes, that’s a good point: making the use of existential types more intentional might make the distinction between generics capabilities and existential capabilities clearer, and make the transition to “Any<…>” less jarring.

Also, I completely skipped discussions of self-conformance and its impact on the generic/existential interaction.

> 
>> Opening existentials
>> 
>> Generalized existentials as described above will still have trouble with protocol requirements that involve Self or associated types in function parameters. For example, let’s try to use Equatable as an existential:
>> 
>> protocol Equatable {
>>   func ==(lhs: Self, rhs: Self) -> Bool
>>   func !=(lhs: Self, rhs: Self) -> Bool
>> }
>> 
>> let e1: Equatable = …
>> let e2: Equatable = …
>> if e1 == e2 { … } // error: e1 and e2 don’t necessarily have the same dynamic type
>> 
>> One explicit way to allow such operations in a type-safe manner is to introduce an “open existential” operation of some sort, which extracts and gives a name to the dynamic type stored inside an existential. For example:
>> 
>> 	 
>> if let storedInE1 = e1 openas T {     // T is a the type of storedInE1, a copy of the value stored in e1
>>   if let storedInE2 = e2 as? T {      // is e2 also a T?
>>     if storedInE1 == storedInE2 { … } // okay: storedInT1 and storedInE2 are both of type T, which we know is Equatable
>>   }
>> }
> 
> Another possibility here is to allow for path-dependent types based on a 'let' binding:
> 
> let e1: Any<Equatable> = ...
> let e2: Any<Equatable> = ...
> // Is e2 the same static type as e1?
> if let e3 = e2 as? e1.Self {
>   return e1 == e3
> }
> 
> let s1: Any<Sequence> = ...
> let s2: Any<Sequence> = ...
> // Are the sequences of the same type? If so, concatenate them.
> var x: [s1.Element] = []
> if let s3 = s2 as? Any<Sequence where Element == s1.Element> {
>   x += s1
>   x += s3
> }
> 
> A close kin to angle-bracket blindness is type variable blindness. It'd be nice to avoid having to introduce explicit local type variables.

Yeah. I love that this eliminates the ugly “openas” operation and its inherent scoping. I think developers will understand the need to pull a value into a let binding, operate on it, then push it back rather than working with the nested types of a variable directly.

	- Doug


-------------- next part --------------
An HTML attachment was scrubbed...
URL: <https://lists.swift.org/pipermail/swift-evolution/attachments/20160302/3636ea80/attachment.html>


More information about the swift-evolution mailing list