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?
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.
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)
.
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))
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With