Support for protocols as parameters for constrained generics

protocol P {...}

protocol P1: P {...}
protocol P2: P {...}
protocol P3: P {...}
...

Class A<T: P> {
    func foo(_ arg: T) {...}
    ...
}

let a = A<P1>()

Compiler: using P1 as a concrete type conforming to P is not supported.

Workaround(with the same protocols):

Class A {
    func foo(_ arg: P1) {...}
    ...
}
Class B {
    func foo(_ arg: P2) {...}
    ...
}
Class C {
    func foo(_ arg: P3) {...}
    ...
}

This is, obviously, a classic example of what happens when we can’t use generics.

There are some nuances concerning this. If we use protocols as constrained generic parameters, we have to:

  • Somehow indicate that the parameter is protocol-only, or else (in the case of several such parameters) there would be no consideration for interaction of unrelated concrete types, conforming to those protocols, and, therefore, senseless code.

Here is an example of the above mentioned nuance: problem-in-extensions-of-protocols-with-associatedtypes

  • Obviously, we also have to restrict inheritance constraints (class type constraints) in such parameters. Fortunately, the Compiler already accounts for this

Maybe adding a specifier to indicate that the parameter is a protocol would be a decent idea:

class Foo<protocol P: Constraint>

EDIT

The need to pass protocols, refining some protocol P, as generic parameters can be bypassed if the reason for such a need is the ability to pass protocols refining P with equal by structure requirements yet different concrete types. Expressible via a single protocol with an identical to the latter structure and necessary associatedtype requirements, in other words.

Solution

I had a completely different use case where I wanted a similar constrain. That said the following code I'm going to demonstrate is not possible right now, and maybe won't be possible at all if we cannot justify it for Swift 5 and find some implementers (I'll bring back the discussion of it very soon, just to see if we can tackle it for Swift 5). The issue I had: SR-6109

So what you really want is a different kind of a constrain that will allow you to use protocols as concrete types.

class A<T> where AnyType<T> : AnyType<P> { ... } // `AnyType<...>` does not exist in Swift

AnyType<...> is a fixed version of metatypes in Swift (current system isn't that flexible) and it allows you to express that you want an existential that conforms to generic parameter type, which is P in your case. The short story is that we messed up our proposal before Swift 3 dropped and couldn't really tackle the revised version after Swift 3. You can find the inheritance and conformance behavior in "Visual metatype relationship example (not a valid Swift code)" section of the revised proposal.


PS: The where AnyType<T> : AnyType<P> is a constraint which means that T should be a type that refines P, such as protocol P1 : P {}. This behavior is described with the relationship of their metatypes.

Hello Adrian. I looked through your issue. It really is an analogous constraint, but for an associated type. I didn't generalize my proposal for associated types because they are quite different to regular generics (they are strictly concrete and therefore, as a person here SR-6109 mentioned, non-existential) and should be considered separately or in the boundaries of a larger generics enhancement topic.

I revised 0126-refactor-metatypes and found it a bit confusing in some places, but that is a slightly different subject.

Classes and structs, though, allow existentials as generic parameters, but only if there are no constraints.

Swift has a very intuitive, neat and standard generics syntax and I am confident that resolving namely this issue doesn't require metatype refactoring.

Personally, I would be more happy to write

class Some<T: P> {...}
// or
class Some<protocol T: P> {...}

rather than the pseudocode example you provided:

class Some<T> where AnyType<T> : AnyType<P> {...}

Doesn't it look simpler and more neat? :)

Although I will not deny that your proposal might be a key for some larger generics enhancement.

Furthermore, as far as I understand, 0126-refactor-metatypes doesn't account for unwanted concrete type interaction when allowing existentials as constrained generic parameters (constrained with existentials, obviously).

We don't want something like this to happen, regardless of the syntax:

class A<T: Equatable, P: Comparable> {

    func foo1(_ arg: P) { foo2(arg) }

    func foo2(_ arg: T) {}
}

let a = A<String, Int>() 

Notice what happens if foo1 is called on a. As such, the compiler rightly spits an error. Something very similar to:
Cannot invoke foo2 with an argument list of ( P )

I propose to eliminate such possibilities by adding a protocol specifier directly to a generic parameter to indicate the existentiality of this parameter:

class A<protocol T: Equatable, protocol P: Comparable> {...}

Thus, we are restrained to using only protocols for T and P and the behavior in class A can be allowed.

I would not support that syntax, because it goes against the current status quo and one would assume you can also do something like class Some<class T : P> (which we already had as class Some<T : class, P> and moved changed to class Some<T> where T : AnyObject, P) or class Some<struct T : P> (which should be really class Some<T> where T : AnyValue, P). You could quickly come up with AnyProtocol but this one is really weird because it really would not fit into the metatype relationship. If every protocol conforms implicitly to AnyProtocol it would also mean that every type that conforms to a protocol would also conform to AnyProtocol which renders it useless and makes it mutual different from the syntax you pitched and even inconsistent in it's behavior to class Some<class T : P>. On the other hand one could say that for protocols it really should be like this Type<Protocol> : AnyType<AnyProtocol>, AnyType<Any> but this is also weird, so I stop here.

AnyProtocol feels almost like Any, which it probably was in the past.

From this example, ignoring the syntax, you can quickly find out what the main issue is. It is a fact that you'd be able to use refinement types of the protocol P in the generic type parameter list. But what for? What is the use case here? You won't be able to implement foo1 and foo2 in class A<protocol T: Equatable, protocol P: Comparable> {...} like you'd wish to even after this proposal for one simple reason, both protocols have Self requirement which requires you to provide concrete types and not refinement protocols. Furthermore not only Self would be an issue but also any associated type on such protocol constraints would render the generic type useless, at least as long we don't have Opened existentials.


What are you trying to solve here? It feels like class A<T> from above should be an abstract generic class or something.

I am not entirely happy with the protocol prefix myself. However, judging by your logic,

protocol P: class {...}

can also assume you can use at least struct instead, and can be considered against the current status quo although it is part of the current syntax and, in my opinion, caught on quite successfully.

Unfortunately, you are right. Protocols with Self or associatedtype requirements are rendered useless in such situations(not true if used as a constraint and with the where clause). The Compiler can just show a standard error in that case:
Protocol P can only be used as a generic constraint because it has Self or associatedtype requirements

If I were using protocols without these requirements though, Class A would be perfectly fine with those methods.

This is a serious protocol-related problem when it comes to writing generic code. You simply keep hitting that wall. opening-existentials gave me some goosebumps tbh, there is an impression people are trying to find wierd ways to seal down associatedtype and Self shortcomings instead of enhancing protocol "generics". Imagine how much better would be to be able to write protocol P<T> {...}. And it is so obvious by the way.

P.S. It looks like quoting doesn't take markdown into consideration... Am I doing something wrong?

As per SE-0156 the : class constraint is deprecated. The deprecation warning simply didn't made it into Swift 4. I encourage you to use : AnyObject in your code if you haven't already. That said, something like : protocol, : struct or : enum won't ever happen in Swift anymore, that ship has sailed, at least syntax wise. Let's see if we can get AnyValue constraint into Swift 5, it's the missing counter part to AnyObject.

Abstract classes were proposed but then deferred from the evolution process. You can read the rationale here.

If you haven't read the whole manifesto yet, the idea of generic protocol was rejected there, or at least it's a highly unlikely feature. In case you really haven't fully read it yet, then please do it to get an idea where Swift is heading. ;)

I'm fixing the code blocks in quotes myself using single or tripple back-ticks.

That is nice to hear! This case is a bit different though, we can't constrain a parameter for it to strictly be an existential in Swift. I actually don't understand how we can express that using the metatype refactoring you promoted earlier in this thread either. If you actually can, I would greatly appreciate a quick explanation.

I misunderstood you here. I didn't mean an abstract class in a c++ manner, simply a generic class :)

Yes, I have looked through the manifesto and am aware of the above mentioned. And I must say the fact that people aren't looking forward to protocol generics makes me feel very frustrated. This would eliminate the need for Self and associatedtype requirements and therefore spare us from all these problems. Not to mention the flexibility it would introduce and how complete generics would become. I don't have a lot of experience in compiler coding, but nevertheless I would love to help implementing it. By the way, this thread's problem would be partially solved as well.

I know where this is coming from, I myself once wanted that feature but quickly learned how to workaround the lack of it nicely. In your case if we had a better version of metatypes we might be able to create your generic-like base protocol.

protocol A {
  // this would at least guarantee that Base must be either a type to conforms to `P` or a refinement protocol
  associatedtype Base where AnyType<Base> : AnyType<P> 
  func foo(_ f: Base)
}

// then create a erasure type which you'd used to store any concrete type that conforms to `A`
struct AnyA { ... }

I'll definitely bring back this discussion soon and look for implementers. I'm just low at spare time atm.

Well our refactored version of metatypes weren't designed to allow a constraint that forces a generic type T to be a class, struct, enum or protocol. In case of classes it could be made possible with the help of AnyObject, because right now there is no other object-type except for classes (but there could and might actually be more in the future). Structs, tuples and enums would be merged by the AnyValue existential. The idea of AnyValue pops up quite frequently but in the past it quickly escalated to a discussion about value semantics instead of simply focusing onto the base value type existential. However I think after some last discussions about async stuff people understood that AnyValue cannot and should not force value semantics out of the box. Furthermore there are also classes that can have value semantics.

Long story short the refactored metatypes would provide more flexibility in generic context (and associated types on protocols) by fixing the relationship between dynamic and static metatypes.

  • dynamic metatype AnyType<T>
  • static metatype Type<T>

The relationship is mostly the same for all metatypes, but it must be a little different in case of protocols though.

Protocols do not conform to them-self, that's why the static protocol metatype does not conform to dynamic metatype with the same generic type parameter but looks like this Type<ProtocolT> : AnyType<Any>. A quick comparison for a class for instance you'd have this Type<ClassT> : AnyType<ClassT>.

Now if you have two protocols a two classes as follows:

protocol P {}
protocol Q : P {}
class A : P {}
class B : Q {}

The metatype relationship according to our revised version could look something like this:

AnyType<P> : AnyType<Any>
Type<P> : AnyType<Any>

AnyType<Q> : AnyType<P>
Type<Q> : AnyType<Any>

AnyType<A> : AnyType<AnyObject>, AnyType<P>
Type<A> : AnyType<A>

AnyType<B> : AnyType<AnyObject>, AnyType<Q>
Type<B> : AnyType<B>

Here is where the magical new constraint is born from.

class Generic<T> where AnyType<T> : AnyType<P> {}

Generic<Q>() // okay
Generic<A>() // okay
Generic<B>() // okay
Generic<P>() // NOT okay 

But again this is all pure theory at this moment.

Thank you very much for the reasoning. Will review it a bit later - I am short in time rn.

For now I want to quickly share how I got around this and maybe(not for sure) convinced myself that the issue in question isn't really that important and can be bypassed. Which... is great!

By playing around with syntax I realized you can use a protocol with associatedtype or Self requirements as a constraint and take advantage of the where clause to tie the associatedtype parameters to the parameters of an enclosing generic class:

protocol P {

    associatedtype Type1
    associatedtype Type2

   ...
}

class A<T, U> {
....

    func foo<R: P>(_ arg: R) where R.Type1 == T, R.Type2 == U {...}
}

This really cheered me up.
Not true generics of course, otherwise it would be this way:

func foo<A, B>(_ arg: P<A, B>) {...}

which can be simulated* with the following (* because T below has to ultimately be concrete(non-existential))

func foo<T: P, U, R>(_ arg: T) where T.Type1 == U, T.Type2 == R {...}

Yet being able to use protocols with associatedtype requirements this way helped me out, namely spared me from declaring multiple protocols for specific types and thus declaring multiple classes for them as well. Now everything is encapsulated in a single generic class.

I think that's a typo but yeah that can work for you ;)

func foo<R : P>(_ arg: R) where R.Type1 == T, R.Type2 == U {...}

Yes, indeed. Thanks for the remark.

FYI: A related issue came up recently for subclass existentials.