10

Here is my code:

protocol SomeProtocol {
}

class A: SomeProtocol {
}

func f1<T: SomeProtocol>(ofType: T.Type, listener: (T?) -> Void) {
}

func f2<T: SomeProtocol>(ofType: T.Type, listener: ([T]?) -> Void) {
}

func g() {
    let l1: (SomeProtocol?) -> Void = ...
    let l2: ([SomeProtocol]?) -> Void = ...
    f1(ofType: A.self, listener: l1) // NO ERROR
    f2(ofType: A.self, listener: l2) // COMPILE ERROR: Cannot convert value of type '([SomeProtocol]?) -> Void' to expected argument type '([_]?) -> Void'
}

What is the problem with the second closure having an argument of an array of generic type objects?

frangulyan
  • 3,580
  • 4
  • 36
  • 64

3 Answers3

8

Swift 4.1 Update

This is a bug that was fixed in this pull request, which will make it into the release of Swift 4.1. Your code now compiles as expected in a 4.1 snapshot.


Pre Swift 4.1

This just looks like you're just stretching the compiler too far.

  • It can deal with conversions from arrays of sub-typed elements to arrays of super-typed elements, e.g [A] to [SomeProtocol] – this is covariance. It's worth noting that arrays have always been an edge case here, as arbitrary generics are invariant. Certain collections, such as Array, just get special treatment from the compiler allowing for covariance.

  • It can deal with conversions of functions with super-typed parameters to functions with sub-typed parameters, e.g (SomeProtocol) -> Void to (A) -> Void – this is contravariance.

However it appears that it currently cannot do both in one go (but really it should be able to; feel free to file a bug).

For what it's worth, this has nothing to do with generics, the following reproduces the same behaviour:

protocol SomeProtocol {}
class A : SomeProtocol {}

func f1(listener: (A) -> Void) {}
func f2(listener: ([A]) -> Void) {}
func f3(listener: () -> [SomeProtocol]) {}

func g() {

    let l1: (SomeProtocol) -> Void = { _ in }        
    f1(listener: l1) // NO ERROR

    let l2: ([SomeProtocol]) -> Void = { _ in }
    f2(listener: l2) 
    // COMPILER ERROR: Cannot convert value of type '([SomeProtocol]) -> Void' to
    // expected argument type '([A]) -> Void'

    // it's the same story for function return types
    let l3: () -> [A] = { [] }
    f3(listener: l3)
    // COMPILER ERROR: Cannot convert value of type '() -> [A]' to
    // expected argument type '() -> [SomeProtocol]'
}

Until fixed, one solution in this case is to simply use a closure expression to act as a trampoline between the two function types:

// converting a ([SomeProtocol]) -> Void to a ([A]) -> Void.
// compiler infers closure expression to be of type ([A]) -> Void, and in the
// implementation, $0 gets implicitly converted from [A] to [SomeProtocol].
f2(listener: { l2($0) })

// converting a () -> [A] to a () -> [SomeProtocol].
// compiler infers closure expression to be of type () -> [SomeProtocol], and in the
// implementation, the result of l3 gets implicitly converted from [A] to [SomeProtocol]
f3(listener: { l3() })

And, applied to your code:

f2(ofType: A.self, listener: { l2($0) })

This works because the compiler infers the closure expression to be of type ([T]?) -> Void, which can be passed to f2. In the implementation of the closure, the compiler then performs an implicit conversion of $0 from [T]? to [SomeProtocol]?.

And, as Dominik is hinting at, this trampoline could also be done as an additional overload of f2:

func f2<T : SomeProtocol>(ofType type: T.Type, listener: ([SomeProtocol]?) -> Void) {
    // pass a closure expression of type ([T]?) -> Void to the original f2, we then
    // deal with the conversion from [T]? to [SomeProtocol]? in the closure.
    // (and by "we", I mean the compiler, implicitly)
    f2(ofType: type, listener: { (arr: [T]?) in listener(arr) })
}

Allowing you to once again call it as f2(ofType: A.self, listener: l2).

Hamish
  • 78,605
  • 19
  • 187
  • 280
  • The problem is that in my real example I have 2 protocols - so I need dozens of overloads. That means that instead to having just one `func parseList(ofType type: T.Type, json: String, listener: ([T]?) -> Void) { return T(fromJson: json) }` I will need to have in addition several `func parseList(ofType type: T.Type, json: String, listener: ([BackendObj1ProtocolForUI]?) -> Void)` so that I can call `parseList(ofType: BackendObj1ImplClass.self, json: replyStr, listener: {(_: [BackendObj1ProtocolForUI]?, _: Error?) in })` for each BackendObj_N concept in my code. – frangulyan Jul 09 '17 at 00:33
1

The listener closure in func f2<T: SomeProtocol>(ofType: T.Type, listener: ([T]?) -> Void) {...} requires its argument to be an array of T, where T is a type that implements SomeProtocol. By writing <T: SomeProtocol>, you are enforcing that all elements of that array are of the same type.

Say for example you have two classes: A and B. Both are completely distinct. Yet both implement SomeProtocol. In this case, the input array cannot be [A(), B()] because of the type constraint. The input array can either be [A(), A()] or [B(), B()].

But, when you define l2 as let l2: ([SomeProtocol]?) -> Void = ..., you allow the closure to accept an argument such as [A(), B()]. Hence this closure, and the closure you define in f2 are incompatible and the compiler cannot convert between the two.

Unfortunately, you cannot add type enforcement to a variable such as l2 as stated here. What you can do is if you know that l2 is going to work on arrays of class A, you could redefine it as follows:

let l2: ([A]?) -> Void = { ... }

Let me try and explain this with a simpler example. Let's say you write a generic function to find the greatest element in an array of comparables:

func greatest<T: Comparable>(array: [T]) -> T {
    // return greatest element in the array
}

Now if you try calling that function like so:

let comparables: [Comparable] = [1, "hello"]
print(greatest(array: comparables))

The compiler will complain since there is no way to compare an Int and a String. What you must instead do is follows:

let comparables: [Int] = [1, 5, 2]
print(greatest(array: comparables))
mohak
  • 603
  • 1
  • 5
  • 11
  • There's no (expected) incompatibility – you can pass a homogenous array with elements of type `T` to something expecting a heterogenous array of some supertype of `T`, i.e you can pass an `[A]` to something expecting a `[SomeProtocol]`. Doing so doesn't break any contracts. – Hamish Jul 08 '17 at 22:30
  • @Hamish From the [swift docs](https://developer.apple.com/library/content/documentation/Swift/Conceptual/Swift_Programming_Language/Generics.html#//apple_ref/doc/uid/TP40014097-CH26-ID181): "The placeholder type name doesn’t say anything about what T must be, but it does say that both a and b must be of the same type T, whatever T represents". So, when we say [T], it must be an array of all the same types. – mohak Jul 08 '17 at 22:41
  • Sure, but what I'm saying is that a `[T]` can be passed to something expecting a `[SomeProtocol]` given `T : SomeProtocol` :) – Hamish Jul 08 '17 at 22:44
  • I absolutely agree. :) The problem is, the questioner is trying to do the reverse. He's passing a closure which accepts [SomeProtocol] when the function is expecting a closure which accepts [T]. – mohak Jul 08 '17 at 22:46
  • But the types are in *parameter* positions, therefore you need to think of it in reverse :) Consider how `f2` would be calling `listener` – it's passing in an argument of type `[T]?` into a function expecting a `[SomeProtocol]?` parameter. – Hamish Jul 08 '17 at 22:47
  • @Hamish yes, that's how I use it. I cannot break anything there in theory, the array I will pass to the listener will have objects of `SomeProtocol` type, it is guaranteed by the rest of the code I wrote, so most probably it is the compiler that is not "smart" enough to understand it. – frangulyan Jul 08 '17 at 22:54
  • Hmmm. But the closure declaration has `[T]`. If `[T]` were used directly as a parameter of `f2`, the "same-type" requirement would be enforced. Why would then the compiler relax that requirement for the closure? – mohak Jul 08 '17 at 22:57
  • @mohak what do you mean by same type? I can call `func f(a: T, b: T)` with `f(a: A(), b: B())` if B inherits A. – frangulyan Jul 08 '17 at 22:59
  • @frangulyan Absolutely yes. By same-type I meant two distinct, non-related classes like an Int and a String. – mohak Jul 08 '17 at 23:00
  • @mohak I guess in case of a closure the logic is indeed reversed, as Hamish wrote. The question is - is it possible to also use that closure in a "direct logic" - if yes, then compiler is correct and cannot do anything, if no - then it should treat closures differently. The first example without an array argument in closure is already treated reversely so seems like the compiler just doesn't go too deep. – frangulyan Jul 08 '17 at 23:04
  • @frangulyan when I write `[T]`, even inside a closure, I would expect the elements to belong to the same type. If the array could contain any type that implements SomeProtocol, I would find that counter-intuitive or at leat a little confusing. I think for your purpose, Dominik's solution seems the best. It works and it's not confusing at all. – mohak Jul 08 '17 at 23:10
  • ok, the "reversed" logic is really misleading, an example makes it simpler to visualize - if `class B: A` and `var x: A = A(); var y: B = B()` and `var xx: (A) -> Void = {_ in }; var yy: (B) -> Void = {_ in }` then `x = y` **but** `yy = xx` :) – frangulyan Jul 08 '17 at 23:10
  • Let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/148706/discussion-between-mohak-and-frangulyan). – mohak Jul 08 '17 at 23:13
0

Have nothing on Hamish's answer, he is 100% right. But if you wanna super simple solution without any explanation or code just work, when working with array of generics protocol, use this:

func f1<T: SomeProtocol>(ofType: T.Type, listener: (T?) -> Void) {
}

func f2<Z: SomeProtocol>(ofType: Z.Type, listener: ([SomeProtocol]?) -> Void) {
}
Dominik Bucher
  • 2,120
  • 2
  • 16
  • 25