Why Can't I Pass a Protocol.Type to a Generic T.Type Parameter

Why can't I pass a Protocol.Type to a generic T.Type parameter?

P.Type vs. P.Protocol

There are two kinds of protocol metatypes. For some protocol P, and a conforming type C:

  • A P.Protocol describes the type of a protocol itself (the only value it can hold is P.self).
  • A P.Type describes a concrete type that conforms to the protocol. It can hold a value of C.self, but not P.self because protocols don't conform to themselves (although one exception to this rule is Any, as Any is the top type, so any metatype value can be typed as Any.Type; including Any.self).

The problem you're facing is that for a given generic placeholder T, when T is some protocol P, T.Type is not P.Type – it is P.Protocol.

So if we jump back to your example:

protocol P {}
class C : P {}

func printType<T>(serviceType: T.Type) {
print(serviceType)
}

let test: P.Type = C.self

// Cannot invoke 'printType' with an argument list of type '(serviceType: P.Type)'
printType(serviceType: test)

We cannot pass test as an argument to printType(serviceType:). Why? Because test is a P.Type; and there's no substitution for T that makes the serviceType: parameter take a P.Type.

If we substitute in P for T, the parameter takes a P.Protocol:

printType(serviceType: P.self) // fine, P.self is of type P.Protocol, not P.Type

If we substitute in a concrete type for T, such as C, the parameter takes a C.Type:

printType(serviceType: C.self) // C.self is of type C.Type

Hacking around with protocol extensions

Okay, so we've learnt that if we can substitute in a concrete type for T, we can pass a C.Type to the function. Can we substitute in the dynamic type that the P.Type wraps? Unfortunately, this requires a language feature called opening existentials, which currently isn't directly available to users.

However, Swift does implicitly open existentials when accessing members on a protocol-typed instance or metatype (i.e it digs out the runtime type and makes it accessible in the form of a generic placeholder). We can take advantage of this fact in a protocol extension:

protocol P {}
class C : P {}

func printType<T>(serviceType: T.Type) {
print("T.self = \(T.self)")
print("serviceType = \(serviceType)")
}

extension P {
static func callPrintType/*<Self : P>*/(/*_ self: Self.Type*/) {
printType(serviceType: self)
}
}

let test: P.Type = C.self
test.callPrintType()
// T.self = C
// serviceType = C

There's quite a bit of stuff going on here, so let's unpack it a little bit:

  • The extension member callPrintType() on P has an implicit generic placeholder Self that's constrained to P. The implicit self parameter is typed using this placeholder.

  • When calling callPrintType() on a P.Type, Swift implicitly digs out the dynamic type that the P.Type wraps (this is the opening of the existential), and uses it to satisfy the Self placeholder. It then passes this dynamic metatype to the implicit self parameter.

  • So, Self will be satisfied by C, which can then be forwarded onto printType's generic placeholder T.


Why is T.Type not P.Type when T == P?

You'll notice how the above workaround works because we avoided substituting in P for the generic placeholder T. But why when substituting in a protocol type P for T, is T.Type not P.Type?

Well, consider:

func foo<T>(_: T.Type) {
let t: T.Type = T.self
print(t)
}

What if we substituted in P for T? If T.Type is P.Type, then what we've got is:

func foo(_: P.Type) {
// Cannot convert value of type 'P.Protocol' to specified type 'P.Type'
let p: P.Type = P.self
print(p)
}

which is illegal; we cannot assign P.self to P.Type, as it's of type P.Protocol, not P.Type.

So, the upshot is that if you want a function parameter that takes a metatype describing any concrete type that conforms to P (rather than just one specific concrete conforming type) – you just want a P.Type parameter, not generics. Generics don't model heterogenous types, that's what protocol types are for.

And that's exactly what you have with printType(conformingClassType:):

func printType(conformingClassType: P.Type) {
print(conformingClassType)
}

printType(conformingClassType: test) // okay

You can pass test to it because it has a parameter of type P.Type. But you'll note this now means we cannot pass P.self to it, as it is not of type P.Type:

// Cannot convert value of type 'P.Protocol' to expected argument type 'P.Type'
printType(conformingClassType: P.self)

SwifT: Generic function that gets generic protocol as parameter not working

I'm not sure why you say "the protocol only demands that Element implements CustomStringConvertible." The protocol demands that echo accept and return its Element, which may or may not be String. For example, I can implement this conforming type:

struct AnotherFoo: FooProtocol {
func echo(element: Int) -> Int { fatalError() }
}

So what should happen if I then call:

callEcho(container: AnotherFoo())

This would try to pass a string literal to a function that requires an Int.

How to pass protocol with associated type (generic protocol) as parameter in Swift?

If you use typealias in a protocol to make it generic-like, then you cannot use it as a variable type until the associated type is resolved. As you have probably experienced, using a protocol with associated type to define a variable (or function parameter) results in a compilation error:

Protocol 'MyProtocol' can only be used as a generic constraint because it has Self os associated type requirements

That means you cannot use it as a concrete type.

So the only 2 ways I am aware of to use a protocol with associated type as a concrete type are:

  • indirectly, by creating a class that implements it. Probably not what you have planned to do
  • making explicit the associated type like you did in your func

See also related answer https://stackoverflow.com/a/26271483/148357

Differences generic protocol type parameter vs direct protocol type

The key confusion is that Swift has two concepts that are spelled the same, and so are often ambiguous. One of the is struct T: A {}, which means "T conforms to the protocol A," and the other is var a: A, which means "the type of variable a is the existential of A."

Conforming to a protocol does not change a type. T is still T. It just happens to conform to some rules.

An "existential" is a compiler-generated box the wraps up a protocol. It's necessary because types that conform to a protocol could be different sizes and different memory layouts. The existential is a box that gives anything that conforms to protocol a consistent layout in memory. Existentials and protocols are related, but not the same thing.

Because an existential is a run-time box that might hold any type, there is some indirection involved, and that can introduce a performance impact and prevents certain optimizations.

Another common confusion is understanding what a type parameter means. In a function definition:

func f<T>(param: T) { ... }

This defines a family of functions f<T>() which are created at compile time based on what you pass as the type parameter. For example, when you call this function this way:

f(param: 1)

a new function is created at compile time called f<Int>(). That is a completely different function than f<String>(), or f<[Double]>(). Each one is its own function, and in principle is a complete copy of all the code in f(). (In practice, the optimizer is pretty smart and may eliminate some of that copying. And there are some other subtleties related to things that cross module boundaries. But this is a pretty decent way to think about what is going on.)

Since specialized versions of generic functions are created for each type that is passed, they can in theory be more optimized, since each version of the function will handle exactly one type. The trade-off is that they can add code-bloat. Do not assume "generics are faster than protocols." There are reasons that generics may be faster than protocols, but you have to actually look at the code generation and profile to know in any particular case.

So, walking through your examples:

func direct(a: A) {
// Doesn't work
let _ = A.init(someInt: 1)
}

A protocol (A) is just a set of rules that types must conform to. You can't construct "some unknown thing that conforms to those rules." How many bytes of memory would be allocated? What implementations would it provide to the rules?

func indirect<T: A>(a: T) {
// Works
let _ = T.init(someInt: 1)
}

In order to call this function, you must pass a type parameter, T, and that type must conform to A. When you call it with a specific type, the compiler will create a new copy of indirect that is specifically designed to work with the T you pass. Since we know that T has a proper init, we know the compiler will be able to write this code when it comes time to do so. But indirect is just a pattern for writing functions. It's not a function itself; not until you give it a T to work with.

let a: A = B(someInt: 0)

// Works
direct(a: a)

a is an existential wrapper around B. direct() expects an existential wrapper, so you can pass it.

// Doesn't work
indirect(a: a)

a is an existential wrapper around B. Existential wrappers do not conform to protocols. They require things that conform to protocols in order to create them (that's why they're called "existentials;" the fact that you created one proves that such a value actually exists). But they don't, themselves, conform to protocols. If they did, then you could do things like what you've tried to do in direct() and say "make a new instance of an existential wrapper without knowing exactly what's inside it." And there's no way to do that. Existential wrappers don't have their own method implementations.

There are cases where an existential could conform to its own protocol. As long as there are no init or static requirements, there actually isn't a problem in principle. But Swift can't currently handle that. Because it can't work for init/static, Swift currently forbids it in all cases.

Swift P.Protocol vs P.Type

P.Protocol is the metatype for the protocol P, just like T.Type is the metatype for the non-protocol type T. There is no "conversions" going on.

So what is P.Type then?

P.Type is an existential metatype. There is no corresponding thing for non-protocol types. Say you want to store metatypes of concrete types that conform to P, you can store it in P.Type:

let x: P.Type = C.self

Note that P.Type can only store metatypes of concrete types, and P.Protocol can only store metatypes of protocols. To see why this division is significant, let's say P defines the static method foo, you can then call x.foo(). This is guaranteed to work because P.Type must have a concrete type, which will implement foo. On the other hand, you can't call foo on P.Protocol, because there is no implementation of foo in P.

See also: https://swiftrocks.com/whats-type-and-self-swift-metatypes

How would I use an array of generics with a type parameter conforming to a protocol?

To see what's wrong with your scenario, forget about trying to declare the type of this array, and try to actually make such an array out of actual objects:

protocol MyProtocol {}
struct MyStruct<T: MyProtocol> {
let myProp: T
}
struct S1 : MyProtocol {}
struct S2 : MyProtocol {}
let myStruct1 = MyStruct(myProp: S1())
let myStruct2 = MyStruct(myProp: S2())
let array = [myStruct1, myStruct2] // error

The compiler kicks back: "heterogeneous collection literal could only be inferred to '[Any]'". And that sums it up. The types of myStruct1 and myStruct2 have nothing in common, so you cannot make an array of them.

That is why you are not able to declare the array to be of a type that will embrace both of them. There is no such type. The types of myStruct1 and myStruct2, namely MyStruct<S1> and MyStruct<S2>, are unrelated.

I know that they look related, because the word "MyProtocol" in the original declaration appears to provide some sort commonality. But the word "MyProtocol" does not designate a type; it designates a constraint on the actual type, saying that whatever this one type is, it must be an adopter of MyProtocol. S1 and S2 are two different types, and so MyStruct<S1> and MyStruct<S2> are two different types. You can't put them together in an array. The fact that both S1 and S2 happen to adopt MyProtocol is irrelevant.

Part of the difficulty may be that you think that two generic types are somehow related because their parameterized types are related. That is not the case. The classic example is a class and its subclass:

class Cat {}
class Kitten: Cat {}
struct Animal<T: Cat> {}
let cat = Animal<Cat>()
let kitten = Animal<Kitten>()
let array2 = [cat, kitten] // error

We get the same compile error. Again, you might imagine that you can put cat and kitten together in an array because Kitten is a subclass of Cat. But that is not true. Animal<Cat> and Animal<Kitten> are unrelated types.



Related Topics



Leave a reply



Submit