Overload resolution for `init` ambiguous but same signature `func` is not. Bug? Intended?

So, Array is at the same level as Thing, but Thing is …more specific than an opaque type??

struct Something<Thing> {
  static func doSomething(_: Thing) {
    print("Thing")
  }

  // The number of restrictions doesn't matter.
  static func doSomething(_: some BidirectionalCollection<Thing> & RangeReplaceableCollection<Thing>) {
    print("Witness")
  }
}

Something.doSomething([()]) // "Thing" 😭

:melting_face:

struct Something {
  static func doSomething<Thing>(_: Thing) {
    print("Thing")
  }

  static func doSomething<Thing>(_: some BidirectionalCollection<Thing> & RangeReplaceableCollection<Thing>) {
    print("Witness")
  }
}

Something.doSomething([()]) // "Witness" 😭😭😭😭😭😭😭

You can get around this for now like this:

struct Something<Thing> {
  static func doSomething<Thingy>(_: Thingy) where Thingy == Thing {
    print("Thing")
  }

  static func doSomething(_: [Thing]) {
    print("Array")
  }
}

Something.doSomething(()) // "Thing"
Something.doSomething([()]) // "Array"

but

Same-type requirement makes generic parameters 'Thingy' and 'Thing' equivalent; this is an error in Swift 6

I used to truly believe that, but now I'm shattered. I don't know what's real anymore. :dotted_line_face:

It seems like a bug to me. As @Jumhyn already mentioned the idea here to see whether one of the types is a "subtype" meaning that one could be called via the other (forwarding the arguments). The position of the Thing shouldn't matter especially because the "self" is the same type here.

I shouldn't reply at night... It should matter because Thing could either be [Int] or Int, there is no subtyping relationship between these two members based on the "self" type because the whole type has to be considered including curried self bit.

The language doesn't allow subtyping relationship between generic parameters which means that subtyping could be only top-level between "self" types and it's possible to forward from B<T> to A<T>, where A and B are generic classes for example, but not from B<T> to A<U>.

1 Like

Ahh right this helps it fit together—the only way we might be able to find a subtype relationship between Something<T> and Something<U> is if T == U, so we eagerly apply that constraint. That makes sense.

It still doesn't quite explain, then, why we find the subtype relationship when comparing doSomething<Thing2>(Array<Thing2>) to doSomething<Thing1>(Thing1) but not when comparing in the other direction. It seems odd to me that the Thing1 == Array<Thing2> admits a solution in only one direction (even though we discover the binding when comparing in both directions).

But it makes sense now how the symmetry is broken by having Thing appear as a type-level generic parameter in one case (and so invariant in the subtyping relationship) and a function-level generic parameter in the other.


Aside: it seems like we apply the same logic to the self types to treat them as equivalent even in cases where we may allow subtyping between generic params, it seems premature to fix the generic arguments as equal prior to subtype solving in a case like this:

extension Array {
    static func f(_ thing: Element) {}
    static func f(_ thing: Array<Element>) {}
}
let listOfThings: [Int] = [3]

Array.f(listOfThings)

An even simpler example:

func f<T>(_: T) where T == Int { // warning: requirement makes 'T' non-generic
    print("Generic")
}
func f(_: Any) {
    print("Any")
}

f(0) // 'Generic'

func g(_: Int) {
    print("Int")
}
func g(_: Any) {
    print("Any")
}

g(0) // 'Int'

I don't think these warnings are wrong in the strictest sense (they are telling you things about the semantics of your generic parameters, not necessarily overload resolution), but I see how they're easily read as "you could remove this generic parameter with no change in compilation behavior." I would just chalk this up as yet another issue with type-based overloading.

The types involved here are:

(Something2<$T0_Thing>) -> ($T0_Thing) -> Void
 ~~~~~~~~~~~~~~~       ~~~~~~~~~~~~
 self                                 doSomething

and

(Something2<$T1_Thing>) -> (Array<$T1_Thing>) -> Void

where $T0_Thing and $T1_Thing are type variables representing generic argument types for generic parameter Thing.

The ranking is checking whether it's possible to forward the arguments from [self.]doSomething(Array<$T1_Thing>) to [self.]doSomething($T0_Thing). To be able to do that selfs have to have subtyping relationship which means that we add a constraint - Something2<$T1_Thing> equal Something2<$T0_Thing> (the equality here is used because top-level types are the same and subtype would decay into equality) since top-level types are equal/subtypes (by virtue of being the same type) the generic arguments a equated ($T0_Thing to $T1_Thing) because they don't support subtyping relationship. Ranking then goes and checks subtyping between parameter types by adding a subtype constraint for each parameter pair - Array<$T1_Thing> subtype $T0_Thing (the direction here is important because we'll be calling first overload from second). Solving this would imply that $T0_Thing has to be both Thing and Array<Thing> which is invalid (in other words calling self.doSomething($T0_thing) from the second method implies a different type for "self" which couldn't be thunked).

In the example where generic parameters appear on the method itself the types look like this:

(Something) -> ($T0_Thing) -> Void

and

(Something) -> (Array<$T1_Thing>) -> Void

Same logic applies, only this time there are no generic parameters which means that the "self" is valid for forwarding.

1 Like

This really helped clarify the mental model for me, thank you! I was thinking too much about comparing the declarations in the context of the actual use we're type checking, rather than looking strictly at the relationship between the declarations themselves (including the curried self types). :slight_smile:

I read

Same-type requirement makes generic parameters 'Thing' and 'Thing' equivalent

to mean that these are the same.

struct Something<Thing> {
  typealias Thing = Thing
  static func doSomething<Thing>(_: Thing) where Thing == Self.Thing { }
  static func doSomething(_: Thing) {}
}

And yet, for now, the code compiles. They're valid overloads, and not even ambiguous at call site; there's just no possible way to call the first one. (At least directly; I bet someone can puzzle out how to get it to be used.)

No, a parallel would not involve Any, but a concrete type. I.e.

func ƒ<T>(_: T) where T == Int { }
func ƒ(_: Int) { }

But while the rules are consistent here (the code compiles, but without a possible way to call the first one), there's no reason to write the first function, so the change coming in Swift 6 doesn't have a meaningful effect here. But losing the ability to constrain where Thing == Self.Thing breaks the ability to write the code that this thread is about.

…That is, unless that constraint is now going to be added invisibly. That seems like it would clear up all the weirdness. Is that how the change is going to be implemented? If not, why not?

struct Something<Thing> {
  typealias Thing = Thing
  static func doSomething<Thing>(_: Thing) where Thing == Self.Thing {
    print("Thing")
  }

  static func doSomething(_: some RandomAccessCollection<Thing>) {
    print("Witness")
  }
}

Something.doSomething([()]) // "Witness" 👍
Something.doSomething(()) // "Thing" 👍
extension Something {
  static func doSomething(_: Thing) {
    print("This non-constrained spelling eats the other two.")
  }
}

Something.doSomething([()]) // Not ambiguous if there's no [Thing] overload. 🫤

I think you’re saying the same thing as @Jumhyn—the wording does make it easy to come to that conclusion, but, at face value, the diagnostic doesn’t strictly say that.

Read strictly, the diagnostic doesn’t say very much at all though: it simply describes the same-type requirement in English. If one were so much as to change one word, the diagnostic might be more meaningful but inaccurate—e.g.: if we say that the same-type requirement makes one of the parameters “redundant,” as the diagnostic strongly implies, that would be false because (as you show) there are non-redundant overflow resolution effects.

This might be a tell that we are diagnosing too broadly if the only thing we can correctly say in the diagnostic is a strict narration of the code being flagged.

Perhaps the correct thing to do here is to narrow the breadth of this diagnostic or make it silenceable. Making it a hard error in Swift 6 without some alternative seems like it might break user code in ways that are difficult to reason about.

2 Likes

I think I'm finally understanding more deeply what you've been trying to say on this topic. It is starting to seem a bit surprising to me that this does work with a generic function. I like that it works, but a little surprising. I think I would still like it to work the same way with type-level generic parameters, but it's not as clear to me that's necessarily the obvious right thing.

Yeah, I can see how might not be so straight-forward to explain the technical concepts it to someone. For what it's worth, I think in actual use, it becomes intuitive fairly quickly (for the most part). It's definitely subjective, but from my perspective as a language user it seemed natural that the generic function case worked, and surprising that the type-level generic init didn't work. I think "more specialized", "more concrete", "more exact (number of parameters, etc...)" gets the general idea across to a language learner without getting into the technical details. Most certainly very subjective though.
Good point about argument labels. I honestly hadn't thought of that. That would greatly simplify the mental model (and language implementation, I imagine). I do think it makes things more complicated from an API user's perspective in some cases, but simplifies things for an API producer.

Yeah, I bet. That sounds incredibly fraught with pitfalls.

@Jumhyn I appreciate your time and patience working through this with me thus far. I feel like I have a more thorough grasp on what's going on here now.

I just tried this, and this causes the ambiguity error to go away, but causes the "Thing" version to be called for every case (Passing Thing, Thing?, and [Thing] all cause the doSomething<Thingy>(_: Thingy) version to be called. But you seem to be getting different results? Am I missing something?

struct Something5<Thing1> {
    static func doSomething<Thing2>(_ thing: Thing2) where Thing2 == Thing1 {
        print("Thing")
    }

    static func doSomething<Thing2>(_ thing: Optional<Thing2>) {
        print("Optional Thing")
    }

    static func doSomething<Thing2>(multi thing: Array<Thing2>) {
        print("List of Things")
    }
}
Something5.doSomething(thing) // Prints: "Thing"
Something5.doSomething(optThing) // Prints: "Thing"
Something5.doSomething(listOfThings) // Prints: "Thing"

No, I never wrote those second two other overloads. They don't seem useful to me…are they?

Yes, two!

  1. A constraint which would allow a concrete Something5 to be defined when the placeholder type isn't being supplied by a function.
  2. An argument label.
Something5.doSomething(()) // "Thing"
Something5<Void>.doSomething(()) // The explicit form of the above.
Something5<Never>.doSomething(()) // "Optional Thing"
Something5<Never>.doSomething(multi: [()]) // "List of Things"

extension Something5 {
  static func doSomething<Thing>(_: [Thing]) {
    print("Unlabeled List of Things")
  }
}

Something5<Never>.doSomething([()]) // "Unlabeled List of Things"
1 Like

Ah! I missed that you didn't have the function-level generic parameter on the Array version. Well, the way I wrote them there, indeed they were not useful because they were un-callable.
This is what I've been trying to achieve:

struct Something6<Thing> {
    init<Thing2>(_ thing: Thing) where Thing2 == Thing {
        print("Thing")
    }

    init(_ thing: Optional<Thing>) {
        print("Optional Thing")
    }

    init(_ thing: Array<Thing>) {
        print("List of Things")
    }
}

Something6(thing) // "Thing"
Something6(optThing) // "Optional Thing"
Something6(listOfThings) // "List of Things"

This finally works now, thanks to your insight, thank you!
I had tried something like this before, but I added the function-level generic parameter and constraint to all three overloads, and of course it didn't work.

This is useful to me to get different behavior/configuration depending on what's passed in while keeping the surface-level API simple from a user perspective.

It's a shame this will be an error in Swift 6 as there's no great way to do this otherwise. Although to @Jumhyn's point, it does seem pretty obscure and not at all intuitive to me.

1 Like

I'm starting to feel like the real answer is maybe to make something like @_disfavoredOverload official (but adding the ability for more than one level of "disfavorment" as it seems was the original intent). I'm sure that's probably not a popular opinion, as it could be abused and be confusing to consumers of API, but in practice, I don't think that would be the case. The Swift community seems to be very mindful about these things as evidenced by the very restrained use of operator overloading. People generally try to do the (subjectively) right thing but as an API writer, being able to have a bit more straight-forward ability to tweak the overload ranking where it makes sense would be very helpful.

I also don't understand why Same-type requirement makes generic parameters 'Thing2' and 'Thing' equivalent will be an error in Swift 6, or why it's even a warning in Swift 5. Is the warning implying this is redundant, and you should just have a single generic parameter for both? It's obviously not if it allows you to express something that couldn't be expressed without it. Does it simplify something in Swift's implementation for it to not be allowed? Not allowing it reduces expressivity in this case.

1 Like

Maybe a better question is, "why didn't it work this way to begin with"? The reasoning might be in @xedin 's answers above, but if so, I'm not understanding it yet.

1 Like

This is requirement machine related diagnostic which might be intentional but I don't remember the reason behind it, might be trying to indicate that there is a loop or one of the generic parameters is simply redundant. /cc @Slava_Pestov

1 Like