How to use constrained protocol syntax in the inheritance clause of a concrete type?

On Xcode14beta5, macOS Ventura 13.0, the following code runs fine:

protocol P { var p: String { get } }
struct S: P { let p="hello world" }

protocol Q {
  associatedtype A
  func f(a: A)
}

struct R: Q {
  typealias A = S
  func f(a: S) { print(a.p) }
}

let r = R()
r.f(a: S()) // Output: hello world

Reading SE-0346 section "Other positions", I thought I should be able to use the new constrained protocol syntax with the primary associated type A as in:

protocol Q<A> {
  associatedtype A
  func f(a: A)
}

struct R: Q<S> {
  //typealias A = S
  func f(a: S) { print(a.p) }
}

but Xcode gave me the error message, "Cannot inherit from protocol type with generic argument 'Q<S>'" on the struct R: Q<S> line. Am I using the new syntax incorrectly? What's the correct usage? Thanks.

2 Likes

My apologies, this is not currently implemented because the behavior of a constrained protocol type here is slightly different than the other positions; as you guessed, it implies a type alias. We should make it work, so that

struct R: Q<S> {}

is equivalent to the second form with the commented out type alias:

struct R: Q {
  typealias A = S
}

In the meantime, you can write the second form.

3 Likes

Cool. Thanks for the clarification.

@Slava_Pestov Is this missing support tracked as an issue in GitHub currently?

3 Likes

@Slava_Pestov is this implemented in Swift 5.9? I think this makes it much easier to translate conformances to dependency injection, e.g.

struct R: Q<S> {}

is much easier to copy/paste as a generic constraint to a function:

function doThing(_ thing: some Q<S>)
1 Like

@Slava_Pestov
Is there a timeline when this might be implemented?

It would safe me a lot of boilerplate if I could write something like this:

protocol ActionInterface {
    associatedtype ReturnType
}

protocol ActionExecutor<Action> {
    associatedtype Action: ActionInterface
    func execute(action: Action) throws -> Action.ReturnType
}

struct MyStringAction: ActionInterface {
    typealias ReturnType = String
}

struct MyIntAction: ActionInterface {
    typealias ReturnType = Int
}

struct Client: ActionExecutor<MyStringAction>, ActionExecutor<MyIntAction> {
    func execute(action: MyStringAction) throws -> String {
        "foo"
    }
    
    func execute(action: MyIntAction) throws -> Int {
        123
    }
}

Right now there is no way to add multiple implementations of the same protocol but with different associated types.

The feature you’re asking for is syntax sugar for declaring a conformance together with a type alias so it doesn’t really permit anything new. The Swift generics model does not permit a concrete type to conform to the same protocol more than once.

But is it really the same protocol if it has a different associated type? In my example all the underlying meta types are different.

Imagine my Client needs to implement 20 different Actions.
If it's considered syntactic sugar, what's the current 'desugared' syntax?

Suppose I write this:

protocol P { associatedtype A }

struct S {}

extension S: P { typealias A = Int }
extension S: P { typealias A = String }

func f<T: P>(_: T) { print(T.A.self) }

f(S())

If this were allowed, would this print Int or String?

1 Like

Sorry, I think I misunderstood your previous message. You were explaining that lifting this limitation wouldn't enable my desired use-case and not that my use-case is syntactic sugar.

But wouldn't this be a prerequisite for a hypothetical dynamic typealias?

protocol P<A> { associatedtype A }

struct S: P { associatedtype A = Double }

extension S: P<Int>  // interpreted { typealias P.Int = Int }
extension S: P<String> // interpreted { typealias P.String = String }

This way print would be well defined.
But I think the crucial question is whether a protocol with an associated type should be considered unique.

When a struct S conforms to a protocol P, it may do so in only one way; that is, it may and must only fulfill each requirement exactly once. This includes the associated type requirement A.

Protocols are not generic, and primary associated types are not generic parameters. In fact, one reason we adopted P<A> syntax for a primary associated type A is precisely because we have no plan to support a generic parameter A using the same syntax.

1 Like