[swift-evolution] PITCH: New :== operator for generic constraints

Slava Pestov spestov at apple.com
Tue Aug 16 20:51:04 CDT 2016


> On Aug 16, 2016, at 6:40 PM, Charles Srstka <cocoadev at charlessoft.com> wrote:
> 
>> On Aug 16, 2016, at 8:13 PM, Slava Pestov <spestov at apple.com <mailto:spestov at apple.com>> wrote:
>> 
>> -1 — this adds a new syntax with little gain, and potentially a lot of additional complexity.
>> 
>>> On Aug 16, 2016, at 2:49 PM, Charles Srstka via swift-evolution <swift-evolution at swift.org <mailto:swift-evolution at swift.org>> wrote:
>>> 
>>> Unfortunately, when this has come up on the list in the past, it has been mentioned that there are some cases where an existential of a protocol does not conform to the protocol itself, so it is impossible to make : always match items that are typed to the protocol.
>> 
>> Indeed, the best solution IMHO would be to implement self-conforming protocols, so that what you’re describing can be expressed without any additional syntax.
>> 
>> The condition for a protocol to be able to conform to itself is the following:
>> 
>> - it must not have any associated type requirements, or contravariant Self in requirement signatures; eg, this rules out the following:
>> 
>>  protocol P { func foo(s: Self) }
>> 
>> - it must not have any static method or initializer requirements
>> 
>> With these conditions met, it would be possible to allow a generic parameter ’T : P’ to bind to a concrete type ’P’.
> 
> Well if it can be done, then that’s great. The reason I thought of a new modifier is because the last time I suggested extending : to include the protocol itself, the reaction was quite negative, suggesting that the amount of work necessary to do that would be outside the bounds of what could be considered reasonable.

The amount of work is certainly not trivial, but this feature request comes up often enough that I think we should try to tackle it at some point.

> I am a little concerned about the second requirement. Protocols that include static methods and initializers work perfectly well inside arrays, and restricting them from generic collections will further discourage use of the latter in favor of the former.

Here is why we must have that requirement. Consider the following code:

protocol P {
  init()
}

struct A : P {
  init() {}
}

struct B : P {
  init() {}
}

func makeIt<T : P>() -> T {
  return T()
}

I can use this function as follows:

let a: A = makeIt() // Creates a new ‘A'
let a: B = makeIt() // Creates a new ‘B’

Now suppose we allow P to self-conform. Then the following becomes valid:

let p: P = makeIt()

What exactly would makeIt() do in this case? There’s no concrete type passed in, or any way of getting one, so there’s nothing to construct. The same issue would come up with static methods here.


> 
>> Note that the type checker work required for this is not very difficult. Indeed, we already allow @objc protocols that don’t have static requirements to self-conform. The real issue is the runtime representation gets tricky, if you want to allow a generic parameter to contain both a concrete type conforming to P, and an existential of P. Basically a generic parameter is passed as three values behind the scenes, the actual value, type metadata for the concrete type, and a witness table for the conformance. To allow the parameter to be bound to an existential type we would need to pass in a special witness table that unpacks the existential and calls the witness table contained in the existential.
>> 
>> It’s even worse if the protocol that self-conforms is a class-bound protocol. A generic parameter conforming to a class-bound protocol is passed as a reference counted pointer and witness table. Unfortunately, a class-bound existential is *not* a reference counted pointer — it has the witness table ‘inside’ the value.
>> 
>> Probably my explanation isn’t great, but really what’s bothering you here isn’t a language limitation, it’s an implementation limitation — once we figure out how to represent protocol existentials efficiently in a way allowing them to self-conform, we should be able to address these use-cases without new syntax.
> 
> What I’ve long wondered is why we don’t have this problem with arrays.
> 
> protocol MyProto {
>     func baz()
>     
>     // Includes static and initializer requirements
>     static func qux()
>     init()
> }
> 
> struct MyStruct: MyProto {
>     func baz() {
>         print("baz")
>     }
>     
>     static func qux() {
>         print("qux")
>     }
>     
>     init() {
>         print("init")
>     }
> }
> 
> func foo(bar: [MyProto]) {
>     for eachMyProto in bar {
>         eachMyProto.baz()
>     }
> }
> 
> let x = [MyStruct()]
> let y = x as [MyProto]
> 
> foo(bar: x)
> foo(bar: y)
> 
> This compiles and runs fine. Why is that?

Recall that an Array is just a (very complex) generic struct in Swift:

struct Array<Element> {
  …
}

The key here is that there are *no generic requirements* placed on the parameter ‘Element’.

So both Array<MyStruct> and Array<MyProto> are perfectly reasonable types, because ‘Element’ can be bound to any type, since there’s nothing you can *do* with an ‘Element’, except for what you can do with all values, which is assign it into a location, load it from a location, or cast it to something.

So binding Element to a protocol type is fine — there’s no witness table of operations passed behind the scenes, because there are no requirements. The representational issue I detailed in my previous e-mail only comes up if additional requirements are placed on the generic parameter.

Hopefully this clarifies things!

Slava

> 
> Charles

-------------- next part --------------
An HTML attachment was scrubbed...
URL: <https://lists.swift.org/pipermail/swift-evolution/attachments/20160816/9d471c7e/attachment.html>


More information about the swift-evolution mailing list