1

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...

Hamish
  • 78,605
  • 19
  • 187
  • 280
CommaToast
  • 11,370
  • 7
  • 54
  • 69
  • what is an idea behind this code? – JuicyFruit Nov 14 '17 at 08:05
  • The compiler is correct – a `(Z) -> Void` is not a `(P) -> Void`. To illustrate why this is the case, let's say `String : P` & `Int : P`. Now let's substitute `Int` for `Z`. We cannot pass a `(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 if that function was actually an `(Int) -> Void`, we'd be trying to pass a `String` to an `Int`, which is clearly unsound. – Hamish Nov 14 '17 at 10:17
  • I disagree. Because the only thing we know about Z at runtime is that it’s a P. Whether or not it’s really an Int or String is totally irrelevant. – CommaToast Nov 15 '17 at 23:06
  • @CommaToast No, at *runtime* `Z` is realised with a concrete type. It is subtype of `P`, but `P` is not a subtype of it, in the same way that `String` is a subtype of `P` in my above example, but `P` is not a subtype of `String`. – Hamish Nov 16 '17 at 06:16

1 Answers1

1

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.

Hamish
  • 78,605
  • 19
  • 187
  • 280
  • In the case where Z === Any

    then I disagree; in this case (Z)-> Void should absolutely be passable as a (P)->Void because the only thing we know about Z is its P-ness anyway! Now in a case where Z has additional constraints that the function knows about, I agree they are not the same, since Any

    is not just P and the function could require Q-ness and R-ness.

    – CommaToast Nov 28 '17 at 01:16
  • So it seems like a case where the compiler is forcing Any

    to not be passable as P, just because Any

    is not passable as P. Which is to throw out the baby with the bathwater.

    – CommaToast Nov 28 '17 at 01:17
  • Nonetheless since your answer explains the (partially flawed) rationale behind the current Swift compiler, I will mark it as correct. – CommaToast Nov 28 '17 at 01:18
  • @CommaToast "*In the case where Z === Any

    *" – that's just one case through; the implementation of the generic function doesn't know what `Z` is at compile time (but `Z` can't be `P` anyway because protocols don't always conform to themselves). This is just as illegal: `func foo(_ x: T, _ p: P) { let y: T = p }`. Remember that Swift generics aren't like C++ templates; the compiler doesn't create individual specialised implementations for each substitution of the generic placeholder(s), it only emits one implementation (though it can specialise as an optimisation).

    – Hamish Nov 28 '17 at 12:36