CommaToast
CommaToast

Reputation: 12178

Why does Swift not allow a type conforming to a protocol to be used in an argument taking the protocol?

Given this example code:

private protocol P {}
final private class X {
    private func j(j: (P) -> Void) -> Void {}
    private func jj<Z: P>(jj: (Z) -> Void) -> Void {
        j(j: jj)
    }
}

Swift 4 in XCode 9.1 gives this compiler error on the line j(j: jj):

Cannot convert value of type ‘(Z) -> Void’ to expected argument type ‘(P) -> Void’.

Why?

Note, it seems to me that it should not give this error, because the type constraint <Z: P> requires that Z absolutely must conform to protocol P. So, there should be absolutely no reason to convert from Z to P, since Z already conforms to P.

Seems like a compiler bug to me...

Upvotes: 1

Views: 273

Answers (1)

Hamish
Hamish

Reputation: 80781

The compiler is correct – a (Z) -> Void is not a (P) -> Void. To illustrate why this is the case, let's define the following conformances:

extension String : P {}
extension Int : P {}

Now let's substitute Int for Z:

final private class X {

  func j(j: (P) -> Void) {
    j("foob")
  }

  func jj(jj: (Int) -> Void) {
    // error: Cannot convert value of type '(Int) -> Void' to expected argument
    // type '(P) -> Void'
    j(j: jj)
  }
}

We cannot pass an (Int) -> Void to a (P) -> Void. Why? Well a (P) -> Void accepts anything that conforms to P – for example, we could pass in a String. But the function that we're passing to j is actually an (Int) -> Void, so we're trying to pass a String to an Int parameter, which is clearly unsound.

If we put the generics back in, it should still be fairly clear why this cannot work:

final private class X {

  func j(j: (P) -> Void) {
    j("foob")
  }

  func jj<Z : P>(jj: (Z) -> Void) {
    // error: Cannot convert value of type '(Z) -> Void' to expected argument
    // type '(P) -> Void'
    j(j: jj)
  }
}

X().jj { (i: Int) in
  print(i) // What are we printing here? A String gets passed in the above implementation..
}

(P) -> Void is a function can deal with any P conforming argument. However (Z) -> Void is a function that can only deal with one specific concrete typed argument that conforms to P (e.g Int in our above example). Typing it as a function that can deal with any P conforming argument would be a lie.

Put in more technical manner, (Z) -> Void is not a subtype of (P) -> Void. Functions are contravariant with respect to their parameter types, meaning that (U) -> Void is a subtype of
(V) -> Void if and only if V is a subtype of U. But P is not a subtype of Z : PZ is a placeholder that is replaced at runtime with a concrete type that conforms to (so is a subtype of) P.

The more interesting part comes when we consider the opposite; that is, passing a (P) -> Void to a (Z) -> Void. Although the placeholder Z : P can only be satisfied by a concrete subtype of P, we cannot substitute P for Z because protocols don't conform to themselves.

Upvotes: 1

Related Questions