Here's a small change to associated type inference that @Douglas_Gregor and I have been discussing. It's source breaking; the Deferred project in the source compatibility suite hits this case.
Introduction
This proposal changes associated type inference behavior to short-circuit a large amount of inference work in the case where the conforming type defines a generic parameter with the same name as an associated type.
This breaks source compatibility with certain valid programs; in Swift 5.1, it is possible for an associated type and a generic parameter with the same name to have different types. However, the name lookup behavior in this case was already very fragile.
Motivation
The Swift language allows the programmer to omit declarations of associated type witnesses inside a conforming type, as long as the associated types can be inferred from other declarations using a set of rules, which are attempted in order.
One important rule states that if we have any value requirements that reference the associated type, we can try to match up the type of the requirement with the type of a witness from the conforming type, and "guess" the associated types as long as the rest of the types match.
For example,
the associated type Bag.Contents
is inferred to be Int
, by matching the type of the protocol requirement takeOut()
with the witness in ConcreteBag
:
protocol Bag {
associatedtype Contents
func takeOut() -> Contents
}
struct ConcreteBag : Bag {
func takeOut() -> Int { return 0 }
}
Another rule checks if the conforming type declares a generic parameter having the same name as the associated type, and attempts to use the parameter as the witness, if one exists.
In Swift 5.1, the value witness rule takes precedence over the generic parameter rule. That is, you can have an associated type with the same name as a generic parameter, but a different type.
Here is an example, using the same Bag protocol as above:
struct GenericBag<Contents> : Bag {
func takeOut() -> [Contents] { return [] }
}
extension GenericBag {
func getContentsType() -> Any.Type {
return Contents.self
}
}
let bag = GenericBag<Int>()
let type: Any.Type = bag.getContentsType()
// type of 'type' is Int.Type
let value: GenericBag<Int>.Contents = bag.takeOut()
// type of 'value' is [Int] not Int
To understand what's going on here, suppose we have a value of type GenericBag<Int>
. The generic parameter Contents
is Int
, but in order for GenericBag.takeOut()
to witness Bag.takeOut()
, we must substitute the associated type Bag.Contents
with [Int]
.
To add to the confusion, Swift 5.1 always introduces an implicit typealias to the conforming type when an associated type was inferred. The existence of this typealias is observable via unqualified name lookup, which leaks out declaration order and compiler implementation details.
Proposed solution
This proposal changes the order in which the two inference rules are applied, so that the generic parameter with the same name as an associated type is always preferred over any other means of inferring the associated type.
Source compatibility
Source compatibility is affected, as in the GenericBag
example above. This proposal does not impact the ability to explicitly define a typealias with the same name as a generic parameter; so the GenericBag
type can still be implemented by explicitly declaring the type witness for Contents
, which will type check under both Swift 5.1 and the language change in this proposal:
struct GenericBag<Contents> : Bag {
typealias Contents = [Contents]
func takeOut() -> [Contents] { return [] }
}
Compatibility of module interface files is not affected. In an interface file, almost all associated type witnesses are explicitly printed out as typealias
declarations. The one exception where a typealias is not printed is specifically the case that is still allowed under this proposal: an associated type that is inferred to be a generic parameter of the same name. Under this proposal, the type witness can be inferred unambiguously while type checking the module interface file.
Effect on ABI stability
This proposal has no effect on ABI stability.
Effect on API resilience
This proposal has no effect on API resilience.
Alternatives considered
One alternative is to stage this change in with a -swift-version
flag. This proposal as written introduces an unconditional breaking change.