Why are generics allowed in Swift protocol declarations but not available in scope?

I’m trying to define a protocol in Swift using generics, like so:

protocol Repository<Entity> {
    associatedtype ID: Hashable

    mutating func save(_ entity: Entity) throws
    func find(by id: ID) -> Entity?
    mutating func delete(_ entity: Entity) throws
}

However, this results in a compiler error: Entity is not found in scope + other errors.

On the other hand, if I write it like this, it works:

protocol Repository<Entity> {
    associatedtype ID: Hashable
    associatedtype Entity

    mutating func save(_ entity: Entity) throws
    func find(by id: ID) -> Entity?
    mutating func delete(_ entity: Entity) throws
}

I have a few questions:

  1. Why does Swift allow generics in protocol declarations if they aren't actually available in the protocol’s scope?
  2. What is the language trying to achieve here—are there plans for something like primary associated types, or is this related to how Swift handles generics and protocols differently?
  3. Is this behavior inspired by something similar to how Kotlin handles generic constructors or type parameters?

I'd appreciate a deeper explanation of what's going on under the hood, or pointers to any official Swift discussions or documentation on this topic.

Edit
By the way, does this mean that generics in protocols are similar to type aliases, where you can omit them and let Swift infer the types from the implementation?

It’s not exactly generics. Associated types is the main mechanism of making generic protocol.

This is what’s called primary associated types and covered by swift-evolution/proposals/0346-light-weight-same-type-syntax.md at main · swiftlang/swift-evolution · GitHub

3 Likes

Aren't there a "An associated type named 'Entity' must be declared in the protocol 'Repository' or a protocol it inherits" error, which gives a clearer view of what's wrong?

No. It has to be declared somewhere.

protocol Umbrella {
associatedtype T1
}

protocol S<T1> : Umbrella {
    func a(in : T1) throws  // T1 definition inherited from Umbrella
    static func b() -> T1   // ditto
    func c<T1>() -> T1    // !!! Type is unrelated to T1. equivalent to c<T>() -> T
}

func d<T2>(_ : T2) -> Float 
    where T2 : S, T2.T1 == OpaquePointer { // S<OpaquePointer> is more sugar-ful
    return T2.c()         // Compiles, calls a c<Float>() that resides under S<OpaquePointer>
}
1 Like

So what is the point of having

protocol S<T1>: Umbrella { }

vs 

protocol S: Umbrella { }

if associatedtype T1 is inherited from Umbrella? You can say

T2.T1 == Whatever 

by having protocol S: Umbrella {...} as signature.

Nay, close, but you must use an instance.

func d(_ t2: some S<OpaquePointer>) -> Float {
  t2.c()
}
1 Like

Ok, make sense.

1 Like

Swift does not support generic protocols.

Because we decided it’s very unlikely that we ever will support generic protocols, we’ve repurposed <…> for a different feature called primary associated types. As the name suggests, this feature allows you to designate one or more of the protocol’s associated types as “primary.”

A primary associated type allows you to express certain constraints that you couldn’t otherwise. For example, you can now write some Collection<Int>, an opaque type, where it’s not possible to write some (Collection where Element == Int).

Don’t confuse primary associated types with generic parameters. Recall that Array<Int> and Array<String> are two distinct types, and there isn’t some base type that’s just Array. By contrast, there is only one protocol Collection, and Collection<Int> and Collection<String> are merely ways of constraining its associated type Element.

As the name suggests, a primary associated type can’t be nominated if there isn’t an associated type of that name declared in the body of the protocol (or inherited).

7 Likes

Great explanation, thanks for clarifying it.

This means that those two are the same. One is syntactic sugar for the other?

func processArray<T: Collection>(_ array: T) -> T where T.Element == Int {
    print("Processing array with \(array.count) elements")
    return array
}

    
func processArray<T: Collection<Int>>(_ array: T) -> T  {
    print("Processing array with \(array.count) elements")
    return array
}
1 Like

What’s the correct terminology for such protocols? I’ve always thought that "generic protocol" is a valid name. To my understanding, existentials have broader meaning?

Yup.

They're simply protocols with primary associated types.

3 Likes