22

I was working with Swinject and a problem is bugging me. I have been stuck one this for almost an entire day. I suspect this is due to Swift being a statictly typed language but I'm not entirely sure.

I summarized my problem in this playground

protocol Protocol {}

class Class: Protocol {}

let test: Protocol.Type = Class.self

func printType(confromingClassType: Protocol.Type) {
    print(confromingClassType)
}

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

print(Class.self) // "Class"
printType(serviceType: Class.self) // "Class"
print(test) // "Class"
printType(confromingClassType: test) // "Class"

printType(serviceType: test) // "note: expected an argument list of type '(serviceType: Service.Type)'"

I tried different solutions like test.self or type(of: test) but none of them work.

So I guess I can't call a function with a generic parameter provided as a variable ?

Hamish
  • 78,605
  • 19
  • 187
  • 280
Guillaume L.
  • 979
  • 8
  • 15
  • What exactly is it that you want to do? – avismara Jul 21 '17 at 09:40
  • Are you trying to have a function that accepts a Class instance as a parameter, in other words Swift equivalent of Java's `String myFunc(Class> classInstance)` – nstosic Jul 21 '17 at 09:54
  • The problem you're facing is that with a generic placeholder `T`, when `T` is a protocol `P`, `T.Type` is `P.Protocol` (metatype that describes the protocol itself) and *not* `P.Type` (metatype that describes a concrete type that conforms to `P`). What's the concrete problem you're trying to solve here? – Hamish Jul 21 '17 at 10:50
  • I'm not sure to completely understand what you mean. My example should work. I can call `printType(confromingClassType: )` the only difference is the genericity of the type Service – Guillaume L. Jul 21 '17 at 11:00
  • @GuillaumeL. You can call `printType(confromingClassType:)` with an argument of `test` because it has a parameter of type `Protocol.Type`. `printType(serviceType:)` has *no such* parameter. As said, a `T.Type` parameter for a generic placeholder `T` cannot accept an existential metatype (`P.Type` for some protocol `P`). If it did, you would be unable to say `printType(serviceType: Protocol.self)`, as a `P.Protocol` is not a `P.Type`. Again, what's the *actual* problem you're trying to solve here? – Hamish Jul 21 '17 at 11:11
  • "As said, a T.Type parameter for a generic placeholder T cannot accept an existential metatype (P.Type for some protocol P)." Why it doesn't ? It is a generic type, that's the point of it, being generic ! What I'm trying to do here is calling the function `printType(serviceType:)` with only classes that I know are implementing Protocol. – Guillaume L. Jul 21 '17 at 12:35
  • @GuillaumeL. "*What I'm trying to do here is calling the function `printType(serviceType:)` with only classes that I know are implementing Protocol*" – then you want a `Protocol.Type` parameter, exactly as you have with `printType(confromingClassType:)`. Generics aren't compatible with what you're trying to achieve; what would you expect `Service.self` to be? If you expect `T.Type` to be `P.Type` when `T` is some protocol `P`, `Service.self` cannot be `Protocol.self`. Why? Because `Protocol.self` is of type `Protocol.Protocol`, not `Protocol.Type`, and your rules said that `T.Type` is `P.Type` – Hamish Jul 21 '17 at 13:28
  • _"What I'm trying to do here is calling the function printType(serviceType:) with only classes **TYPES** that I know are implementing Protocol"_ Sorry I mistyped my answer – Guillaume L. Jul 21 '17 at 13:40
  • @GuillaumeL. I wrote an answer hopefully illustrating why `T.Type` must be `P.Protocol`, and not `P.Type` :) Let me know if you need any further clarification – Hamish Jul 21 '17 at 13:50

2 Answers2

40

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) 
Hamish
  • 78,605
  • 19
  • 187
  • 280
  • 2
    Thank you for your very complete answer, it's not trivial to say the least. So I guess I can't do what I wanted to do, which is a bit sad. Do you think there is a work around or something that should be done in a different way to make that possible ? – Guillaume L. Jul 21 '17 at 15:00
  • 1
    @GuillaumeL. I do agree that the current state of metatypes are far from ideal, and I fully expect them to be overhauled in a future version of the language. Unfortunately, I'm not at all familiar with Swinject, so cannot suggest a current workaround to your problem (I don't know what the metatype parameter is used for). It may well be worth asking a new question focussing on what you're trying to achieve with Swinject (you can always link back to this question for context); and hopefully someone familiar with the framework could offer you a workaround. – Hamish Jul 21 '17 at 15:31
  • 1
    @GuillaumeL. Just thought of a workaround using a little known trick with protocol extensions; might be useful for you. – Hamish Mar 03 '18 at 11:41
  • 1
    Wow, thanks for the update. I had to read my question again to remember what I was talking about. Since then we gave up on this idea but maybe we could try again with your workaround ! – Guillaume L. Mar 05 '18 at 16:34
1

I've ran your code in a playground, and it seems that this is the reason why it wont compile

let test: Protocol.Type = Class.self

If you remove the type declaration for test, the code will work and will print out Class.Type at line 15.

So the following code compiles and runs:

protocol Protocol {}

class Class: Protocol {}

let test = Class.self

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

print(Class.Type.self) // "Class.Type"
printType(serviceType: Class.Type.self) // "Class.Type"
print(type(of: test)) // "Class.Type"

printType(serviceType: type(of: test)) // "Class.Type"

I hope this helps with your problem.


Edit

The error I am getting in the playground with the original code is the following:

Playground execution failed: error: Untitled Page 2.xcplaygroundpage:9:1: error: cannot invoke 'printType' with an argument list of type '(serviceType: Protocol.Type.Type)'
printType(serviceType: type(of: test)) // "Class.Type"

This means you are calling Type 2 times, that's why the code will not compile, because the type you are already calling the method with the argument of type Protocol.Type.

If you change the method's signature like this:

let test: Protocol.Type = Class.self

func printType<Service>(serviceType: Service) {
    print(serviceType)
}

everything will compile and work right, printing Class.type

This is also the reason my first version of the answer will compile, since it will assign the right type for test can call .Type only once.

Catalina T.
  • 3,456
  • 19
  • 29
  • I am curious about what actually prevented this `let test: Protocol.Type = Class.self` – Bista Jul 21 '17 at 10:01
  • It's a nice try but it doesn't really solve the problem because in the app we don't know the type of test, the only thing we know is that it conforms to a protocol. That's why test must be implementing the protocol to reflect the real case. – Guillaume L. Jul 21 '17 at 10:06
  • I can't change the function signature, it's a function from the Swinject framework `public func register( _ serviceType: Service.Type, name: String? = nil, factory: @escaping (Resolver, Arg1) -> Service) -> ServiceEntry` – Guillaume L. Jul 21 '17 at 10:41