Overriding a default “implementation” typealias with another typealias

here is a protocol Indexable, with an associated type requirement Index. it gets a default “implementation” for Index where Self: RawRepresentable, and has a refining protocol IndexableString that provides another default “implementation” for Index where Self: LosslessStringConvertible.

protocol Indexable {
    associatedtype Index
}
extension Indexable where Self: RawRepresentable, RawValue: Indexable {
    typealias Index = RawValue.Index
}

protocol IndexableString: Indexable where Index == String.Index {}
extension IndexableString where Self: LosslessStringConvertible {
    typealias Index = String.Index
}

extension Int8: Indexable {
    typealias Index = Int
}

enum E: Int8 {
    case foobie
    case barbie
}
extension E: Indexable {}

this means if a type is both RawRepresentable and LosslessStringConvertible, there will be two witness candidates, the one from Indexable and the one from IndexableString.

if Index were a function, subscript, or property requirement, this would Just Work, because every IndexableString type is also Indexable, so the more-specific overload is chosen. but for some reason typealiases do not benefit from this disambiguation logic.

enum F: Int8 {
    case foobie
    case barbie
}

extension F: IndexableString {}
// Type 'F' does not conform to protocol 'Indexable'
// Multiple matching types named 'Index'

extension F: LosslessStringConvertible {
    var description: String { "" }
    init?(_: String) { return nil }
}

this is not good! we want to use the type requirement here, as a way of “bundling” a bunch of static members we do not want polluting the namespace of the conforming type. similarly, we want the RawRepresentable shortcut to just “be there”, we don’t want to introduce a third protocol like IndexableByRawRepresentation just to opt into that.

4 Likes