Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Swift Generics: Cannot convert value of type to expected argument type

Tags:

generics

swift

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?

like image 943
frangulyan Avatar asked Jul 08 '17 22:07

frangulyan


2 Answers

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

like image 185
Hamish Avatar answered Nov 11 '22 06:11

Hamish


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))
like image 1
mohak Avatar answered Nov 11 '22 06:11

mohak