Issue with Constrained Overload Resolution

So, I've written a function with(_: update:) that's generic over some type T.

public func with<T>(_ initial: T, update: (inout T) throws -> Void) rethrows -> T {
    var copy = initial
    try update(&copy)
    return copy
}

And also an overload that constrains T to AnyObject

public func with<T: AnyObject>(_ object: T, update: (T) throws -> Void) rethrows -> T {
    try update(object)
    return object
}

I can use the unconstrained version just fine:

let x = with(1) { // This works.
    $0 += 2
    $0 *= 5
    $0 -= 1
}

But if I try and use the constrained overload, I get an "error: ambiguous use of 'with(_:update:)'":

final class Thing {
    var name: String = ""
    var position: (Int, Int) = (0, 0)
}

let y = with(Thing()) { //This causes a compiler error
    $0.name = "Goblin Bandit"
    $0.position = (13, 2)
}

I don't understand why I'm getting this error, because I thought the compiler preferred the most specific overload. What am I doing wrong?

This is ambiguous because the two functions don't have the same signature, so the second is not an overload of the first. Let me write their signatures down right next to each other:

public func with<T>(_ initial: T, update: (inout T) throws -> Void) rethrows -> T
public func with<T: AnyObject>(_ object: T, update: (T) throws -> Void) rethrows

Note that the second with takes a different function signature for update than the first does. This means a function that is valid to pass to with<T> is not valid to pass to with<T: AnyObject>. As a result, the two functions are now just two functions with similar signatures, and so the compiler is not sure which one you want.

You can resolve this in a few ways:

  1. Make the signatures match.

    "Copying" the class doesn't hurt, and nor does passing it as inout, so you can rewrite your overload to:

    public func with<T: AnyObject>(_ object: T, update: (inout T) throws -> Void) rethrows -> T {
        var copy = object
        try update(&copy)
        return copy
    }
    
  2. Annotate the types.

    If your AnyObject-based implementation really wants to avoid the inout, you can resolve the error by helping the compiler out a bit:

    let y = with(Thing()) { (t: Thing) in
        t.name = "Goblin Bandit"
        t.position = (13, 2)
    }
    
    

Either of those approaches works.

In short, of course they are overloads of each other, but for the compiler to prefer the most specific one based on generic constraints, the only difference between the overloads must be the generic signature, i.e.

func foo<T>(_ arg: T) {...}

func foo<T: Collection>(_ arg: T) {...}

I'm not sure though whether in-out-ness should be considered.

IMO your example should be unambiguous, as the AnyObject constrained overload should be considered more constrained than the unconstrained overload (feel free to file a bug).

The compiler however finds it to be ambiguous due to the fact that overload ranking currently only considers protocol constraints when deciding whether one declaration is "more constrained" than another.

For example, this works:

public func with<T>(_ initial: T, update: (inout T) throws -> Void) rethrows -> T {
  var copy = initial
  try update(&copy)
  return copy
}

final class Thing {
  var name: String = ""
  var position: (Int, Int) = (0, 0)
}

public protocol P : class {}
extension Thing : P {}

public func with<T : P>(_ object: T, update: (T) throws -> Void) rethrows -> T {
  try update(object)
  return object
}

let y = with(Thing()) { // fine.
  $0.name = "Goblin Bandit"
  $0.position = (13, 2)
}

And although AnyObject is documented as the protocol to which all class types implicitly conform, it's actually modelled in the compiler as an empty protocol composition (with a bit set to indicate AnyObject). The upshot of this is that it isn't treated as a protocol constraint when ranking and therefore doesn't count towards the "more constrained" score of an overload (but IMO it should).

The reason why it's unambiguous if the inout-ness is the same, for example:

public func with<T>(_ initial: T, update: (inout T) throws -> Void) rethrows -> T {
  var copy = initial
  try update(&copy)
  return copy
}

public func with<T : AnyObject>(_ object: T, update: (inout T) throws -> Void) rethrows -> T {
  var object = object
  try update(&object)
  return object
}

final class Thing {
  var name: String = ""
  var position: (Int, Int) = (0, 0)
}

let y = with(Thing()) { // fine.
  $0.name = "Goblin Bandit"
  $0.position = (13, 2)
}

is because the latter function's parameter list is considered a subtype of the former's parameter list, and therefore the latter is considered "more specialised". There's no subtype relation if the inout-ness of the parameters differ though, such as in your example.

5 Likes

Thank you, that was a very good explanation of what's going on. I've filled a bug here: SR-8725