Lifting the "Self or associated type" constraint on existentials


(Joe Groff) #1

When the first seed of Swift came out many years ago, there were technical reasons for the "self or associated type" constraint on protocol existentials: At that time, protocol witness tables did not carry associated type information, so it was impossible to re-open the dynamic type in order to dispatch methods on an existential when a protocol had associated types. This was fixed a while ago in order to allow for recursive protocol constraints, but we held on to the restriction on existentials thinking it would help avoid confusion or design dead ends with people using protocols as existentials in ways that we couldn't really fully support yet. However, nowadays we also have protocol extensions, so even if a protocol doesn't have any core requirements with contravariant Self or associated type arguments, contravariant protocol methods can be added by extensions, so the type system issues unavoidably exist already. The "self or associated type" constraint also creates artificial resilience problems for protocols; it would otherwise be OK for a protocol to introduce new associated type or contravariant method requirements with default implementations, except that doing so would revoke the protocol's ability to be used as an existential type. Since the restriction is pretty much artificial at this point, I think we should just lift it. What do you all think?


Pattern matching Errors
(Matthew Johnson) #2

Can you be more specific about what you have in mind? Would this be generalized existentials or would this be a really basic first step where members with self or associated type constraints just are not available? Either way, +1 from me. Now is as good a time as any to start down this road.


(Thomas Krajacic) #3

Would you happen to have a simple example of how this applies in some code? Your explanation is a tad too abstract for me to follow otherwise :flushed:


(Joe Groff) #4

This wouldn't be fully generalized existentials because you still can't put constraints on associated types in the existential. We'd follow the existing rule which is that protocol members that use contravariant Self or associated types aren't available on the existential. (You could however write protocol extension methods, which implicitly open the Self type, and use associated types indirectly in a way that's then available through the existential type.)


(Matthew Johnson) #5

Sounds like a nice first step forward towards the larger goal of supporting constraints. Hopefully this would alleviate some of the fear people have about "PATs".


(Paul Cantrell) #6

Yes please.


(Mox) #7

Is this about allowing:

protocol P {
  associatedtype A
}

func f(param: P) {
  //..
}

If that is the case, then FINALLY! Huge yes to this.


(Tanner) #8

Huge +1 to this. Would make using protocols in Swift so much easier


#9

If I'm understanding the pitch correctly +1. By far the biggest roadblock I run into when writing modern Swift. I'd interested to hear if there are any known downsides though.


(Slava Pestov) #10

To clarify though, this pitch would not actually allow you to use members of the protocol that involve Self or associated types. So they would still be second class in a sense.


(アンドレカンドレ) #11

super +1 from me ^^


(Adrian Zubarev) #12

Just to clarify, this pitch won‘t eliminate the need for opaque types? I assume that these kind of existentials won‘t work im generic context like proposed by opaque types.


(Cory Benfield) #13

This seems like a good idea to me Joe: the limitation is fairly artificial, as you say, and in general I don't think the proposed new behaviour will be any more surprising than the current behaviour.


(David Hart) #14

Yes please! This would be exceptional!

Question:

protocol P {
    associatedtype A
    var a: A { get }
}

func f(param: P) {
    let a = param.a
    // What's the type of a? P.A?
    // What can we do with A? Nothing?
}

protocol Q {
    associatedtype B: CustomStringConvertible
    var b: B { get }
}

func g(param: Q) {
    let b = param.b
    // What's the type of b? Q.B? CustomStringConvertible?
    print(b.description) // Is this valid?
}

(Karl) #15

Given what Slava said above, I'm guessing you wouldn't be able to call param.a or param.b.

You would be able to create arrays of P or Q though, and pass those elements in to functions (must be non-generic due to protocol existentials not self-conforming) or use them directly when their superclass constraints allow it.

A more complete solution would also lift that self-conformance limitation. I wonder if we could...


(Thierry Passeron) #16

+1 for
var array: [P]


(Adrian Zubarev) #17

This will allow [Equatable] and create some more confusion, but hey I‘m still +1 for this :slight_smile:


(Joe Groff) #18

It's awkward, but you can in fact "open" the existential by invoking a protocol extension method, which will give you Self as the dynamic type of the value inside the method, which you can then pass to generic functions:

func foo<T: P>(_: T)

extension P {
  func _forwardToFoo() { foo(self) }
}

func callFooOnEach(_ ps: [P]) {
  for p in ps {
    p._forwardToFoo()
  }
}

Another nice stepping stone on the way to generalized existentials (which I'm not proposing immediately) would be to make it so that existentials automatically open when passed as generic arguments to functions. Oftentimes this is closer to what you want than protocol self-conformance, since you really want to operate on the value inside the existential, not the existential itself.


(Adrian Zubarev) #19

Wait :scream: you can pass Self to generic context today? :exploding_head: That‘s interesting to know.


Probably a compiler bug: can't convert Range<Int> to Sequence where Element == Int
(Matthew Johnson) #20

This is a pretty awesome trick I have used a few times. Unfortunately, because Swift does not have higher-ranked types we cannot pass a generic function to call. The good news is that we don't have to hard code the function that gets called (foo in the above example). There are many variations on this trick and a very useful one to know is how to use a "receiver" protocol to do double dispatch:

protocol _ReceiveP {
    func _receiveP<T: P>(_: T)
}

extension P {
    func _openP<Receiver: _ReceiveP>(for receiver: Receiver) {
        receiver._receiveP(self) 
    }
}

extension Bar {
    func doSomethingWithPs(_ ps: [P]) {
        for p in ps {
            p._openP(for: self)
        }
    }
  
    func _receiveP<T: P>(_ value: T) {
        // do somehing with value
    }
}

I hope you can imagine several variations on this theme. For example, you can pass dynamic context through the dispatch, you can have a receive method that returns a value which is directly returned by the open method, etc.