ZfHxFr
(Zf Hx Fr)
1
protocol P1<A> {
associatedtype A
var a: A { get }
}
func foo<T>(p: any P1<T>) {
// a is inferred as T, which is expected.
let a = p.a
}
// However, if we constrain the associated type in the P1 protocol.
protocol P1<A> {
associatedtype A: P2
var a: A { get }
}
protocol P2 {}
func foo<T>(p: any P1<T>) {
// a is inferred as P2.
let a = p.a
}
Is this a bug, or intended?
I think the inferred type should always be the more specific one.
The fix is to make T constrained to P2 explicitly.
func foo<T: P2>(p: any P1<T>) {
// a is inferred as T, which is expected.
let a = p.a
}
Maybe the Xcode should provide a warning or fix-it for this situation.
ksluder
(Kyle Sluder)
2
P2 is more specific than T. In the first foo, T is a free type variable. In the second, T is a type variable with an upper bound of P2. Both of these are less specific than P2 itself.
ZfHxFr
(Zf Hx Fr)
3
Isn't T, as a concrete type, more specific than P2 as an existential?
ksluder
(Kyle Sluder)
4
Not in the generic implementation, no. Itās a type variable that will be provided with a concrete type at runtime.
jrose
(Jordan Rose)
5
Right, T is probably a better choice (because it has strictly more information, which is clear since it can be coerced to any P2), but from the type checkerās perspective, any P2 is a fully-known, concrete typeā¦that happens to be a protocol type. And changing this could be source-breaking, unfortunatelyāimagine if the local were a var and had another value assigned to it later. (I donāt know if itās the kind of thing that you could change in a language version without having two separate type checkers, either; youād have to have a type checker expert weigh in on that.)
ksluder
(Kyle Sluder)
6
I donāt understand your argument. For the purposes of type inference, T is P2. T unifies with P1.A. P1.Aās minimum upper bound is P2. Therefore T unifies with P2.
jrose
(Jordan Rose)
7
āUpper boundā is not āunifies withā, as evidenced by
func id<Value: Equatable>(_ value: Value) -> Value {
return value
}
ZfHxFr
(Zf Hx Fr)
8
Maybe your statement is incorrect.
In my understanding, the concrete type of a generic type parameter is known statically at the function's call site.
ksluder
(Kyle Sluder)
9
Are you referring to the type context within id, or the type context of the caller of id? Perhaps Iām abusing terminology by calling this āunificationā, but within idās type context the polytype forall Value . Value isSubtypeOf Equatable must be reduced to a simple monotype. The only candidate is Equatable. (Edit: as @jrose explains, this doesnāt mean that Value unifies with Equatable throughout the entirety of Foo.) In the callerās context, Value is unified with the static type of the argument.
Yes but this information canāt cross into the implementation of foo. That would make Foo no longer generic, and it is impossible to satisfy in the general case:
protocol P1<A> {
associatedtype A: P2
var a: A
}
protocol P2 { }
struct One: P1 {
struct InnerOne: P2 { var bar: Int }
var a: InnerOne
}
struct Two: P1 {
struct InnerTwo: P2 { var quux: String }
var a: InnerTwo
}
foo(One())
foo(Two())
func Foo<T>(p: P1<T>) {
// at runtime, this function is invoked once with T == One.InnerOne and once with T == Two.InnerTwo.
// none of this information is available *statically*.
// at compile time, all that we know is that T is a subtype of P2.
// therefore any expression of type T can only be treated as āsomething that conforms to P2.ā
}
Remember, Swift generics are not textual substitutions like C++ templates are. You canāt write p.quux and rely on SFINAE to avoid a type error. Swift says āno, the only operations available on p are the ones declared by P2.ā
jrose
(Jordan Rose)
10
This is just not true. Hereās another example:
func meaningless<A: Equatable, B: Equatable>(a: A, b: B) {
assert(a == b) // error, obviously!
var local = a
assert(local == a) // valid
local = b // error!
}
Static type parameters are not erased just because we only know their bounds. (Iām not sure whether it could work the way youāre describing without Self type requirements, but it doesnāt.)
ksluder
(Kyle Sluder)
11
Aha, now I understand what youāre saying. Iāve been incorrectly generalizing from expressions involving p to the entire type environment of the function.
That said, the part you quoted isnāt actually the wrong part! Itās the part that comes immediately after thatās wrong: the resulting monotype is a new anonymous type thatās a subtype of Equatable, not the Equatable type itself.
1 Like
ZfHxFr
(Zf Hx Fr)
12
I suggest you test your example in Xcode, then you will find that the type of T is inferred as One.InnerOne and Two.InnerTwo statically.
Actually, you cannot call a generic function if the concrete types of its type parameters cannot be determined.
For this specific example, the underlying type of p is dynamic, but T should be static.
And I updated my original post with a fix that allows p.a to be inferred as T.
ksluder
(Kyle Sluder)
13
A type parameter cannot be statically inferred as two different types.
ZfHxFr
(Zf Hx Fr)
14
respectively for the two foo calls
ksluder
(Kyle Sluder)
15
Thatās not what you asked about. You asked about the type of T within body of foo.
Jumhyn
(Frederick Kellison-Linn)
16
That adding the P2 constraint explicitly to the generic parameter list of the function changes the type resolution behavior internally to the function smells like a bug to me⦠Iād expect that constraint to get inferred anyway from the use of T as primary associated type argument to P1. Seems like something is going a bit wrong with the erasure rules. cc @hborla for after the holidays.
1 Like