Search code examples
swiftgrammarswift-protocols

Swift protocols with generic arguments


Swift noob here. Consider this Swift 5.7 code:

import Foundation

// This is according to the grammar.
protocol TestProtocol1 {
    associatedtype T
    associatedtype U
}

// Not allowed by the grammer, but still compiles.
protocol TestProtocol2<T> {
    associatedtype T
    associatedtype U
}

// Doesn't seem to matter if I add one or both type arguments.
protocol TestProtocol3<T, U> {
    associatedtype T
    associatedtype U
}

// This is fine. As expected.
class TestClass1 : TestProtocol1 {
    typealias T = Int
    typealias U = Bool
}

// Fine too. Even though I don't specify the type arguments.
class TestClass2 : TestProtocol2 {
    typealias T = Int
    typealias U = Bool
}

// error: cannot inherit from protocol type with generic argument 'TestProtocol3<Int, Bool>'
class TestClass3 : TestProtocol3<Int, Bool> {
    typealias T = Int
    typealias U = Bool
}

Questions:

  • Is there any semantic difference between the three protocol definitions?
  • Why does it compile when declaring the associated types as generic arguments when the grammar doesn't allow it?
  • Why is it useful to (probably redundantly) add type arguments to protocols? For example, protocol Sequence<Element> does this too.

Solution

  • The "generic parameters" that you are seeing are the protocols' primary associated types, proposed in SE-0346, implemented in Swift 5.7, I suppose the grammar section in the language reference just hasn't been updated yet.

    The three protocol declarations are semantically different, in that they have different primary associated types. When using the protocol in certain positions, primary associated types are what you can directly specify in <...>, rather than specify them somewhere else like in a where clause. For example, when using the protocol as a generic constraint:

    func foo<P: TestProtocol2<Int>>(p: P) { ... }
    

    is syntactic sugar for:

    func foo<P: TestProtocol2>(p: P) where P.T == Int { ... }
    

    The former is just a little more concise :)

    You cannot do something similar with TestProtocol1, because it doesn't have primary associated types.

    For TestProtocol3, you must specify both primary associated types:

    func foo<P: TestProtocol3<Int, Bool>>(p: P) { ... }
    

    According to the SE proposal, this syntax was also planned to be usable in the protocol conformance clause of a concrete type, like in your code:

    class TestClass3 : TestProtocol3<Int, Bool> { // does not compile
    

    However, this feature did not get added for some reason. You can still use it in the inheritance clause of a protocol though:

    protocol TestProtocol4: TestProtocol3<Int, Bool> { } // works
    

    See the SE proposal for more details.