Search code examples
swiftgenericsoption-typeoverload-resolution

Swift optional promotion vs generic overload resolution


Please consider the following code:

protocol P {}
class X {}
class Y: P {}

func foo<T>(_ closure: (T) -> Void) { print(type(of: closure)) }
func foo<T>(_ closure: (T) -> Void) where T: P { print(type(of: closure)) }

let xClosure: (X?) -> Void = { _ in }
foo(xClosure)   //  prints "(Optional<X>) -> ()"
let yClosure: (Y?) -> Void = { _ in }
foo(yClosure)   //  prints "(Y) -> ()"

Why does the foo(yClosure) call resolve to the version of foo constrained to T: P? I understand why that version prints what it prints, what I don't see is why it gets called instead of the other one.

To me it seems that the non-P version would be a better match for T == (Y?) -> Void. Sure, the constrained version is more specific, but it requires conversion (an implicit conversion from (Y?) -> Void to (Y) -> Void), while the non-P version could be called with no conversion.

Is there a way to fix this code in a way such that the P-constrained version gets called only if the parameter type of the passed-in closure directly conforms to P, without any implicit conversions?


Solution

  • Specificity seems to always trump variance conversions, according to my experiments. For example:

    func bar<T>(_ x: [Int], _ y: T) { print("A") }
    func bar<T: P>(_ x: [Any], _ y: T) { print("B") }
    
    bar([1], Y()) // A
    

    bar is more specific, but requires a variance conversion from [Int] to [Any].

    For why you can convert from (Y?) -> Void to (P) -> Void, see this. Note that Y is a subtype of Y?, by compiler magic.

    Since it is so consistent, this behaviour seems to be by design. Since you can't really make Y not a subtype of Y?, you don't have a lot of choices if you want to get the desired behaviour.

    I have this work around, and I admit it's really ugly - make your own Optional type. Let's call it Maybe<T>:

    enum Maybe<T> {
        case some(T)
        case none
    
        // implement all the Optional methods if you want
    }
    

    Now, your (Maybe<Y>) -> Void won't be converted to (P) -> Void. Normally I wouldn't recommend this, but since you said:

    in the real-world code where I encountered this, the closure has multiple params, any of them can be optional, so this would lead to a combinatorial explosion.

    I thought reinventing Optional might be worth it.