Generics: compiler cannot choose between T and T? return types

Last line: compiler shows error "Ambiguous use of 'resolveReturn(id:)`". However similar functionality works if used inside of closure parameter instead of return type.

class Container {
    private var storage: [String: Any] = [:]
    
    func register<T>(id: String, _ value: T) {
        storage[id] = value
    }
    
    func resolveClosure<T>(id: String, _ resolver: (T) -> Void) {
        let value = storage[id] as! T
        resolver(value)
    }
    
    func resolveClosure<T>(id: String, _ resolver: (T?) -> Void) {
        let value = storage[id] as? T
        resolver(value)
    }
    
    func resolveReturn<T>(id: String) -> T {
        return storage[id] as! T
    }
    
    func resolveReturn<T>(id: String) -> T? {
        return storage[id] as? T
    }
}

let container = Container()
container.register(id: "RightKey", 1)

container.resolveClosure(id: "RightKey") { (value: Int?) -> Void in
    print(value) // prints "Optional(1)
}

container.resolveClosure(id: "WrongKey") { (value: Int) -> Void in
    print(value) // runtime error
}

container.resolveClosure(id: "WrongKey") { (value: Int?) -> Void in
    print(value) // prints "nil"
}

let iWrong: Int = container.resolveReturn(id: "WrongKey") // runtime error
let iRight: Int? = container.resolveReturn(id: "WrongKey") // Ambguous use of 'resolveReturn(id:)`
1 Like

Does adding as Int? possibly help?

Nope, same result.

At a high level, this is happening because the compiler doesn't know whether to call resolveReturn<T>(id: String) -> T with T bound to Int?, or resolveReturn<T>(id: String) -> T? with T bound to Int. Both of these are valid solutions, so it will come down to the compiler's solution ranking to determine whether there's a "true" ambiguity.

You can get some idea of where this fails by examining the output of swiftc -Xfrontend -typecheck -Xfrontend -debug-constraints <file> and locating the line number of the expression(s) in question.

In particular, this lets us see where solving for the resolveClosure version is able to rank one solution as better than the other:

comparing solutions 1 and 0
Comparing declarations
func resolveClosure<T>(id: String, _ resolver: (T?) -> Void) {
  let value


}
and
func resolveClosure<T>(id: String, _ resolver: (T) -> Void) {
  let value


}
(isDynamicOverloadComparison: 0)
  ($T0 potentially_incomplete bindings={(subtypes of) T?})
  Initial bindings: $T0 := T?
  (attempting type variable $T0 := T?
    (attempting disjunction choice T? bind $T1? [deep equality] [[locator@0x7f7cba86fe68 [ -> function argument]]];
      (found solution 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0)
    )
  )
comparison result: better
Comparing declarations
func resolveClosure<T>(id: String, _ resolver: (T) -> Void) {
  let value


}
and
func resolveClosure<T>(id: String, _ resolver: (T?) -> Void) {
  let value


}
(isDynamicOverloadComparison: 0)
(failed constraint ($T1) -> Void subtype ($T0?) -> Void [[locator@0x7f7cac825c00 []]];)
comparison result: not better

Since the two available declarations compared as "better" in one direction and "not better" in the other, the compiler picks the declaration that compared as "better." However, if we find the same point in the solving process for the resolveReturn line:

Comparing declarations
func resolveReturn<T>(id: String) -> T? {
  return 
}
and
func resolveReturn<T>(id: String) -> T {
  return 
}
(isDynamicOverloadComparison: 0)
  (found solution 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0)
comparison result: better
Comparing declarations
func resolveReturn<T>(id: String) -> T {
  return 
}
and
func resolveReturn<T>(id: String) -> T? {
  return 
}
(isDynamicOverloadComparison: 0)
  (found solution 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0)
comparison result: better

Here, the declarations compare as "better" in both directions, meaning that we can't say for sure that one is definitively better than the other.

Beyond that, the output is pretty difficult to parse for me, so I can't say for sure why the declarations can be properly ranked in one case but not the other. Perhaps @hborla or @xedin could shed a bit of further light on this.

2 Likes

What you see here is ranking attempting to check whether parameter in one overload could be used to cover the second one i.e. Int could be passed to Int? so the overload that accepts T? is more universal in a sense. But that applies only to parameters, there is, intentionally, no such rule for result types because they are contextually dependent.

2 Likes