Why can you access an associated type but not a generic placeholder from a metatype instance?

If you have a generic type,

enum Type<T> { }

you can't access the placeholder.

_ = Type<Int>.self.T // Type 'Type<Int>' has no member 'T'

But make it an associated type, and then you can. (The previous line will compile.)

protocol Protocol { associatedtype T }
extension Type: Protocol { }

I don't understand why that works that way yet. What about you?

1 Like

Generic parameters are not visible to qualified lookup (foo.bar); they’re not members, but type alias declarations are. Where does the type alias come from?

Here’s a bit of terminology. The concrete implementation of an associated type is called a type witness. The type witness can be an explicit type alias or nested type:

protocol P {
  associatedtype A
  func f() -> A
}

struct S1 : P {
 typealias A = Int
 func f() -> Int {…}
}

struct S2 : P {
 struct A {}
 func f() -> A {…}
}

There is a language feature called associated type inference which allows you to omit the type alias in some cases where it can be deduced. For example:

struct S3 : P {
 func f() -> Int {…}
}

A generic parameter that has the same name as the associated type is then picked up as a special case:

struct S4<A> : P {
 func f() -> A {…}
}

In all cases associated type inference synthesizes a type alias declaration, as if you had declared the type witness explicitly.

So S4 is equivalent to this:

struct S4<A> : P {
 typealias A = A
 func f() -> A {…}
}

Which is why you can refer to S4<String>.A, which resolves to just String.

Note that from inside the body of S4, A refers to the generic parameter, while Self.A is the type alias. This can cause the following slightly confusing situation:

struct S5<A> : P {
 // synthesized: typealias A = Int
 func f() -> Int {…}
}

Now S5<String>.A resolves to Int and not String. Changing this would be a source break, but perhaps it could be banned under a future language mode.

4 Likes

And if you want to vend out the generic parameter into inner type, you can do it manually.

enum Type<T> { 
    typealias T = T
}

Type<Int>.T.self == Int.self

But I'm not sure about shadowing here, I would rather use name that doesn't shadow the generic parameter.

1 Like

I’m wondering if there is ever a real reason to want to do this, though. If the type alias is not a type witness, you can’t “abstract” over it, you can only refer to it directly via a concrete base type like Foo<Bar>.A. Why not just write Bar? (Incidentally, you can’t override a type alias in a subclass either, so this trick really doesn’t give you much.)

Yep. I did it rather as an illustration...

1 Like

I don't know if there's ever a need for it. I gave this some thought after someone said key paths had poor usability.

If I had to work with this…

struct S { var value: Int? }

func setValueIfMatch(instance: S, value: some Any) {

…I would do it like

func cast<Uncast, Cast>(
  _ uncast: Uncast, to: Cast.Type = Cast.self
) throws(CastError) -> Cast {
  guard case let cast as Cast = uncast else { throw .init() }
  return cast
}
struct CastError: Error { }
try? instance.value = cast(value)

…but it seems odd to me that this isn't an option:

if let value = try? cast(value, to: type(of: \S.value).Value) {
  instance.value = value
}

…without

extension KeyPath {
  typealias Value = Value
}
1 Like