"Typealias overriding associated type" fixit breaks code

Aha OK. The trouble here is that I can't default it for the end user who conforms - which is the attraction of typealias. I get to "set it" and they don't have to declare it and make sure it matches the types I've specified (which will be hard for them to discover?) in order to get the functionality.

Instead they'll suffer obtuse call site errors won't they, saying it can't find a function matching the types used.

I'm not sure I follow. In both cases conformers don't have to declare a corresponding typealias. Can you provide a minimal example illustrating the issue?

Hi @anthonylatsis

Sorry for the months that have passed here. I got busy with with releasing the open source framework that is suffering this false warning.

Here's a concise example, it works as a playground:

protocol Engine {
    var capacity: Int { get }
}

class ElectricMotor: Engine {
    let capacity: Int = 0
}

protocol Car {
    associatedtype EngineType: Engine
}

protocol ElectricCar: Car {
    func startCharging()
}

extension ElectricCar {
    // Xcode sugggests a fix it to remove this and constrain extension on EngineType.
    // This is not what we want. We want to provide the default type so types conforming
    // to ElectricCar do not need think about this.
    typealias EngineType = ElectricMotor
}

class NissanLEAF: ElectricCar {
    func startCharging() {
        print("Charging...")
    }
}

So the typealias EngineType = ElectricMotor line triggers the bad warning. The intent here is precisely to specify the type in the extension, so that types conforming to the sub-protocol do not need to specify the associated type or even be aware of it.

This works very well, aside from the irritating warning from the compiler.

If you apply the fix it, you then have to specify the typealias in every type that conforms to ElectricCar, which is very unfortunate. Protocol oriented programming is meant to make things easier. And it does... except for the Fix It that isn't a Fix It.

@Slava_Pestov it looks like you are very much the winner. Perhaps the Fix It wording could be improved to allude to this solution?

I see you at WWDC I will thank you in person!

I do not replicate this.

When I copy your code into a playground in Xcode 10.2.1, I see the warning you describe. However, when I click the fix-it, everything works properly.

In particular, the fix-it removes the typealias from the extension, and it changes the declaration of ElectricCar to be:

protocol ElectricCar: Car where EngineType == ElectricMotor { … }

Thereupon, the code works with no warnings and no errors. To be abundantly clear, it works without having to specify the associatedtype in conforming types.

Thank you @nevin!

You are correct. I was being confused by two different places where the warning was happening in my code. Once of the simple locations (as per the example provided about the car) did indeed work fine witth the fix-it — which is rather counter intuitive as the syntax implies that the conforming type will still have to declare the type to meet the constraint, but it appears it does not. Re-stating the associatedtype as per @Slava_Pestov's suggestion makes more sense (as does, IMO a typealias). I guess the associatedtype redeclaration still leaves it open for subtypes to redefine the associated type which may or may not be desirabled.

The other place in my code where this was happening is much more difficult to express simply. Essentially there is an extension that provides helper functions that are generic over the "inherited" protocol's associated types, where one of the associated types is a type that is generic over another associated type of the protocol. It's easy enough to grok in code, but hard to explain (see early github link - re: IntentType, IntentResponseType, PresenterType etc.)

It was possible to get it working with associatedtype, by changing the extension to be constrained explicitly on the type declared in the re-stated associatedtype (but this meant helper functions had to use the explicit type also declared in the associatedtype).

However changing the protocol to have the constraint instead (as per fix-it) and then updating the extension functions to use the expected protocol types works a dream. I am a little concerned about weird it is to grok that adding a constraint on a protocol actually seems to define the associated type, it seems counter-intuitive. Are there docs that cover this behaviour?

FYI here's the PR on my framework that shows the changes:

....and I spoke too soon.

Consumers my API cannot conform to the type - unless they explicitly typealias the associated type, even though the extension is constrained on that same type. Above where I questioned how adding the protocol constraint would define the associated type for conforming types.. seems like it doesn't, and shouldn't.

So I think this solution only works if you explicitly typealias which is what I was trying to save my users from.

I am very mindful of how hard this use case is to explain in isolation, it pains me. I will try again.

Could you provide a minimal example which produces the error?

Yes. I have prepared three variants of the same sample. I am trying to further simplify it, but these are a start.

First up, here's the case we're talking about - following the fix-it recommendation, which requires defining the associatedtype in the final conforming class. Which is the opposite of the intention:

import Foundation

// ****************************************************************************************
//
// Example of trying to supply a default type for an associated type in a sub-protocol
// and helper functions that use this default type in an extention on that sub-protocol.
// The natural way to achieve this by defining the typealias in the sub-protocol produces
// a compiler warning and fix-it suggesting use of protocol constraint.
//
// This approach: constraining by the associated type, as per fix-it
//
// Outcome: Fail. This requires explicitly defining the associated type in the final conforming type,
// so fails to achieve the default type behaviour, and the protocol/extension constraints
// are likely pointless/overcomplicating as a result
//
// ****************************************************************************************

// The base type with an associated type we want to constraing/default to later

protocol Action {
    associatedtype PresenterType
    
    static func perform(presenter: PresenterType)
}

// Types specific to Intent-implementing Action(s)

class IntentResponse {
}

// A presenter that IntentAction's use to relay their strongly typed response to the callback
class IntentPresenter<ResponseType> where ResponseType: IntentResponse {
    let completion: (ResponseType) -> Void
    
    public init(completion: @escaping (ResponseType) -> Void) {
        self.completion = completion
    }
    
    /// Call to pass the response to Siri
    public func showResponse(_ response: IntentResponse) {
        // Ugly but we're at the intersection of static typing and the liberal arts
        guard let safeResponse = response as? ResponseType else {
            fatalError("Wrong response type, expected \(ResponseType.self) but got \(type(of: response))")
        }
        completion(safeResponse)
    }
}

// The subtype of Action that is for Siri Intent actions to conform, to get default behaviours

// This uses the protocol constraint approach suggested by Fix-It
protocol IntentAction: Action where PresenterType == IntentPresenter<IntentResponseType> {
    associatedtype IntentResponseType: IntentResponse
}

// Provide the default implementations we want conforming types to receive without effort
extension IntentAction where PresenterType == IntentPresenter<IntentResponseType> {
    static func perform(withCallback callback: @escaping (IntentResponseType) -> Void) {
        // Create the correct type of presenter automatically
        let presenter = IntentPresenter(completion: callback)
        // Now pass it to the underlying Action.perform call
        perform(presenter: presenter)
    }
}

// Use case, an intent action

// This will be our response type that we pass to the presenter
class MyIntentResponse: IntentResponse {
    let status: Int

    init(status: Int) {
        self.status = status
    }
}

final class DoSomethingWithSiriAction: IntentAction {
    typealias IntentResponseType = MyIntentResponse
    
    // This is required, unsurprisingly. However the point of doing all this using a `typealias`
    // (which WORKS!) produces a Fix-It warning
    // Comment this line out to get the error about PresenterType not being defined
    typealias PresenterType = IntentPresenter<IntentResponseType>
    
    // Provide Action.perform impl here, and set the response on the IntentPresenter
    // that is our PresenterType
    static func perform(presenter: PresenterType) {
        presenter.showResponse(MyIntentResponse(status: 42))
    }
}

let intentCallback = { (response: MyIntentResponse) in
    print("Response is: \(response)")
}
DoSomethingWithSiriAction.perform(withCallback: intentCallback)

The typealias PresenterType = ... line is the part we don't want to add (and don't need to by defining the typealias in IntentAction itself - but it produces a warning)

Here's the variant trying to use @Slava_Pestov's approach to re-state the associated type. It fails because we can't call the base protocol's perform() function as the PresenterType is seemingly not visible or I'm holding it wrong. This is even with the explicit typealias in MyIntentAction which we are trying to avoid in the first place.

Here's a much simplified example which shows that re-stating the associated type doesn't work:

import Foundation

protocol A {
    associatedtype InputType: CustomStringConvertible
    associatedtype OtherType = Void
}

struct GenericThing<T> where T: CustomStringConvertible {
    let value: T
}

protocol RefinedA: A {
    associatedtype SomeType: CustomStringConvertible
    associatedtype OtherType = GenericThing<SomeType>
}

extension RefinedA where OtherType == GenericThing<SomeType> {
    static func doSomething(other: OtherType) {
        print(other.value.description)
    }
}

final class MyType: RefinedA {
    typealias InputType = String
    typealias SomeType = String
    // This is required otherwise `doSomething` is not found, even though it is set in `RefinedA`
    // typealias OtherType = GenericThing<SomeType>
}

let other = GenericThing<String>(value: "Hello" as String)
MyType.doSomething(other: other)

Here, the compiler complains that doSomething does not existt on MyType. This is clearly because the extension on RefinedA is constrained on OtherType and it is not seeing the associatedtype re-statement in the declaration of RefinedA. If I explicitly define OtherType in MyType it works.

This seems like a bug?

I note that the original Swift 2.2 associatedtype proposal says tthis:

It is not clear that concrete type aliases are forbidden inside protocols.

This seems to be what I'm trying to do, and yet the compiler does not say this is forbidden, and seems to support it despite the fix-it warning. Conversely associatedtype = XXXX "re-assignment" does not appear to set the associated type such that subtypes see the definition.

Here is my original typealias in IntentAction variant that works fine, compiles fine, but we get this warning (the cause of this whole thread) which erroneously tells me to use an associated type.

Either I'm doing something Swift is not meant to support or this warning is wrong for this use case. This is the truth I am after as I'm trying to ship a 1.0 of my open source library that wants to supply this default typealias so consumers of the API don't have to.

FYI @Slava_Pestov @Nevin @anthonylatsis I got to the bottom of this. The key was using associatedtype, and constraining the extension to the exact type (==) specified in the associatedtype assignment, and using the exact types in the extension functions:

public protocol DismissingUIAction: UIAction {
    associatedtype InputType = DismissUIInput
    associatedtype PresenterType = UIViewController
}

public extension DismissingUIAction where InputType == DismissUIInput, PresenterType == UIViewController {
    static func perform(context: ActionContext<DismissUIInput>, presenter: UIViewController, completion: Completion) -> Completion.Status {
        presenter.dismiss(animated: context.input.animated)
        return completion.completedSync(.successWithFeatureTermination)
    }
}

Thanks for your support and patience.

I still have the feeling there are a couple of misunderstandings going on, but am glad the question is resolved. By the way, the last simplified example you provided compiles fine with the Swift 5 compiler. Here's a shorter version:

struct Generic<T: CustomStringConvertible> {}

protocol P {
    associatedtype Assoc1 = Void
}

protocol RefinedP: P {
    associatedtype Assoc1 = Generic<Assoc2>
    associatedtype Assoc2: CustomStringConvertible
}

extension RefinedP where Assoc1 == Generic<Assoc2> {
    static func foo(arg: Assoc1) {}
}

final class Foo: RefinedP {
    typealias Assoc2 = String
}

// Test
Foo.foo(arg: Generic<String>())

OK I showed the code and the issue to @Slava_Pestov at WWDC Swift labs and we established that this is a Swift type inference bug.

We created the relevant JIRA for improving the type inference so that protocols with where constraints that completely specify the associated types as Slava confirmed that subtypes and extensions should not have to also specify these constraints when the sub-protocol has refined them in the where clause.

He also gave me a workaround for now of specifying them as associated types as well as constraints for now, which suppresses the warning, and means that extensions and subtypes don't have to also specify them.

I'm afraid I still fail to understand the exact problem.. At least the latest examples compile fine with Swift 5. Could you share a link to the SR? I suspect a terminological confusion between us; hopefully the bug description can clarify the issue for me. Thanks.

https://bugs.swift.org/browse/SR-10831

Marc, the snippet you attached compiles in Swift 5. Are you using an older language version
(< Xcode10)?

I've updated the issue with more context. It was created with Slava sitting next to me, and the sample was suggested by him!

I see, apologies.

@Slava_Pestov Slava, can you please double-check whether this SR actually describes a relevant bug?

You're right, the example does indeed compile with the Swift 5 compiler. Marc's code however did not, and it was a very similar setup. Here is another case that does not work:

struct G<T> {}
  
protocol P {
  associatedtype T
  associatedtype U
}

protocol Q: P where T == G<U> {}

protocol R: Q where U == Int {}

struct X: R {}

Marc, if I recall the project was open source. Can you share a link to the code?

Thanks, that clarifies a lot. It seems to me the core of the issue is the protocol hierarchy itself, because it works if you swap those constraints. I've managed to boil down one of Marc's examples to what probably is the very similar setup we are after:

struct G<T> {}

protocol P {
    associatedtype U
}

protocol Q: P where U == G<T> {
    associatedtype T
}

struct Foo: Q { // Type 'Foo' does not conform to protocol 'P'
    typealias T = Int
}