Why is `[P1(), P2()]` not inferred as `[any P]` when passed to a function expecting `some Sequence<any P>`?

I encountered an interesting behavior in Swift.

protocol User {}
struct Buyer: User {}
struct Seller: User {}

func opaque(_ sequence: some Sequence<any User>) {}

// Error: Cannot convert value of type '[Any]' to expected argument type '[any User]'
opaque([Buyer(), Seller()])

The expression [Buyer(), Seller()] is not inferred as [any User] when passed to a function that expects some Sequence<any User>. Of course, it works if I explicitly cast it as [any User].

At first, I thought this might be due to the primary associated type, but the same issue occurs with de-sugared regular generics.

func generics<T: Sequence>(_ sequence: T) where T.Element == any User {}
// Error: Cannot convert value of type '[Any]' to expected argument type '[any User]'
generics([Buyer(), Seller()])

However, it works as expected when using an array type:

func array(_ sequence: [any User]) {}
array([Buyer(), Seller()]) // âś…

Can anyone explain why the inference doesn't work in this case?

swift --version
swift-driver version: 1.90.11.1 Apple Swift version 5.10 (swiftlang-5.10.0.13 clang-1500.3.9.4)
Target: arm64-apple-macosx14.0
2 Likes

I've seen this kind of thing but don't understand it.

Aside from what you said, here are workarounds:

func existential(_: any Sequence<any User>) { }
existential([Buyer(), Seller()])
extension Sequence<any User> {
  var cast: Self { self }
}

opaque([Buyer(), Seller()].cast)
opaque([Buyer() as any User, Seller()])
1 Like

We don't attempt to infer a common existential type from a heterogenous list of concrete types in an array literal like that. There's no good way of doing this that's efficient, unambiguous and doesn't lead to surprising results (imagine getting random types like any CustomStringConvertible showing up, etc). An explicit type annotation is best in this case.

7 Likes

I think there's a bit of an implicit question here as to why we don't backwards-infer the type of the collection as Array<any User> based on the type of the sequence argument and the fact that we know how Array conforms to Sequence, which would then let the any User type propagate down to the collection literal. Is generic parameter inference via associated type witnesses with a generic function like this just something that doesn't happen?

Edit: oh, I misread--some Sequence<any User> is the version that doesn't work, it actually does work with any Sequence<any User> as @Quedlinbug mentions!

6 Likes

Given this information, I am not understanding why the change from some to any Sequence works. Did I miss something in what you said?

Does it have to do with castability?

[Buyer(), Seller()] as any Sequence<any User>

I think the compiler wouldn’t even need that step in this case:

  1. Elements are convertible to $T (unknown, unconstrained)
  2. Array literal type is unconstrained
  3. Contextual type is a Sequence with an Element of any P

This is ambiguous, so we apply the heuristic of using Array for the literal.

  1. Array has a conformance to Sequence
  2. Propagate the Sequence constraint back to the Array’s witness for the associatedtype Element
  3. That’s $T; now we have $T == any P
  4. Check each element is convertible to any P

There are a lot of search paths the compiler doesn’t take because they require an arbitrary intersection search for a common type, but this doesn’t seem to be one of them, because the common type is provided (as it would be if the target type was Array<any P>). So I think the question stands.

All that said, changing type inference in any way is risky, because there could be multiple overloads and therefore making more code “work” potentially means making existing working code ambiguous. :-( Weighing that decision is not something I envy you type checker engineers!

9 Likes

Thank you for sharing your knowledge, type checker engineers ;).

That's make sense to me.
Personally, I just wanted to understand the behavior of the inference system. So, I’m satisfied now that I know the cause and the sense of the situation.

Thank you!

for this specific example, can anyone help decipher the output when built with the -Xfrontend -debug-constraints flag, to better explain the 'why' of what's going on?

to my untrained eye, it seems like it does something like:

  1. attempts to bind $T8 (the array literal argument type variable) to Array
  2. decides this works, and introduces a new variable, $T10 for the array's Element
  3. attempts to bind $T10 to Any
  4. eventually decides this doesn't produce a 'true' solution, but stops anyway to 'salvage and emit diagnostics'.

so is this a case in which the solver just happens to 'discover' a solution that requires a 'fix' before one that does not, and so we end up with an error? (tangentially: i'd also be interested to better understand how the solver explores the search space, and why/when it decides to stop).

@jrose does this interpretation of your comment seem accurate?

  • the system of constraints in this instance is underspecified, so some heuristics need to be applied.
  • the current heuristics end up with an error that requires adding additional constraints into the system.
  • in theory some new heuristic(s) could be added to the constraint solver to alter the outcome in this instance, and nudge it toward the 'right' solution.
  • such changes are risky though as they may break existing code in ways that are difficult to predict.

Without digging into the code, my best guess of what's going on here is something like:

  • The some Sequence<any User> version implicitly creates a generic function which changes the structure of the constraint system and the relationship between all the type variables we're trying to resolve (because a each generic parameter introduces a type variable we need to solve for).
  • OTOH the compiler considers any Sequence<any User> to be a concrete type, which doesn't introduce any type variables.
  • Some decisions in the compiler about which type bindings to attempt or which inference rules to apply at each step depend on whether a given binding contains unsolved type variables at that moment in the solving process.
  • Because of the above, the order in which the type system applies its inference rules might be different in each case.
  • If the default array literal inference rules get applied before the element type gets propagated properly, we will not have the context necessary to pick anything other than Any for the array element type.
3 Likes