0

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?

imre
  • 1,667
  • 1
  • 14
  • 28
  • That is really strange. I actually wouldn't expect this to compile at all since you are defining the closure arguments with optionals. It's even stranger, though, that even though yClosure defines the closure argument as an optional, when printed, it claims the argument is not an optional. While this may not answer your question, what you can do is define two more foo functions that take closures with optionals. That will give you the results you are looking for. But I don't think that should be necessary. – Rob C Jun 11 '20 at 05:51
  • 1
    @RobertCrabtree Once the compiler selects the P-constrained version of `foo`, losing the optional-ness can be explained, I think. `Y?` doesn't conform to `P`, but `Y` does, so that version of `foo` can only be called by doing `T == Y` (as opposed to `T == Y?`) and converting the closure from `(Y?) -> Void` to `(Y) -> Void`. This conversion is valid and can happen implicitly (`let cl: (Y) -> Void = yClosure` works too, also https://forums.swift.org/t/implicit-promotion-of-optional/12381/4). But I still don't see why that overload of `foo` gets selected in the first place. – imre Jun 11 '20 at 06:04
  • @RobertCrabtree Adding extra overloads for `closure: (T?) -> Void` works, but isn't ideal, because 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. But in this simple case that works. – imre Jun 11 '20 at 06:06

1 Answers1

1

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.

Sweeper
  • 213,210
  • 22
  • 193
  • 313