"Typealias overriding associated type" fixit breaks code

The following code produces the "Typealias overriding associated type" warning for the typealias extension RawRequestEncodable.

import Foundation

protocol RequestRouter { }
struct Router: RequestRouter { }

protocol Requestable {
    associatedtype Parameters: Encodable
    associatedtype Response: Decodable
    
    func route() -> RequestRouter
    func parameters() -> Parameters
}

extension Requestable where Self: Encodable {
    func parameters() -> Self { return self }
}

protocol RawRequestEncodable: Requestable {
    associatedtype RawRequest: Requestable
    
    func asRawRequest() -> RawRequest
}

extension RawRequestEncodable {
    typealias Response = Self.RawRequest.Response
    
    func route() -> RequestRouter {
        return asRawRequest().route()
    }
    
    func parameters() -> Self.RawRequest.Parameters {
        return asRawRequest().parameters()
    }
}

struct BaseRequest: Encodable { }

extension BaseRequest: Requestable {
    typealias Response = String
    
    func route() -> RequestRouter {
        return Router()
    }
}

struct Request { }

extension Request: RawRequestEncodable {
    func asRawRequest() -> BaseRequest {
        return BaseRequest()
    }
}

Applying the fixit produces this code:

protocol RequestRouter { }
struct Router: RequestRouter { }

protocol Requestable {
    associatedtype Parameters: Encodable
    associatedtype Response: Decodable
    
    func route() -> RequestRouter
    func parameters() -> Parameters
}

extension Requestable where Self: Encodable {
    func parameters() -> Self { return self }
}

protocol RawRequestEncodable: Requestable where Response == Self.RawRequest.Response {
    associatedtype RawRequest: Requestable
    
    func asRawRequest() -> RawRequest
}

extension RawRequestEncodable {
    func route() -> RequestRouter {
        return asRawRequest().route()
    }
    
    func parameters() -> Self.RawRequest.Parameters {
        return asRawRequest().parameters()
    }
}

struct BaseRequest: Encodable { }

extension BaseRequest: Requestable {
    typealias Response = String
    
    func route() -> RequestRouter {
        return Router()
    }
}

struct Request { }

extension Request: RawRequestEncodable {
    func asRawRequest() -> BaseRequest {
        return BaseRequest()
    }
}

This code has an error, as now Request doesn't conform to RawRequestEncodable.

Now, using the suggested pattern from here where I redeclare the associatedtype Response in RawRequestEncodable fixes the issue and gets rid of the warning, but it seems like this fixit is buggy at least. Or is there a better way to write what I want here, where types conforming to RawRequestEncodable only have to implement asRawRequest()?

Also, editing this code causes compiler crashes. Xcode 10.1.

Your conformance breaks because when you switch from a type alias that simply shadows an associated type to a same-type constraint on an associated type, you enter associated type inference territory. Associated type inference is limited to a single level for performance reasons, meaning that inherited associated types (Response, Parameters) will not be inferred. The Generics Manifesto has a few words on this over here. A short example to illustrate the limitation:

protocol P {
  associatedtype A
}

protocol P1: P {
  associatedtype B
  
  func foo(arg: A) -> B
}

class Foo: P1 { // Type 'Foo' does not conform to protocol 'P'
  func foo(arg: Int) -> Bool { return true }
}

Restating the associated type with an assignment only defines or overrides a default value (type) for that associated type. That said, if a default value is not what you want, there are two options: either explicitly write down the type alias witnesses upon conformance, or keep typealias Response = Self.RawRequest.Response with the warning.

Thanks, having only one layer of type inference explains it. Luckily the redeclaration works in way that gives me the dynamism I need. Do you think it's worthwhile to file a bug about the warning and fixit? It seems both confusingly worded and dangerous given it can break working code.

Clearly the way the fixit is presented rehashing all that code in between isn't great when the protocol declaration and the type alias are far apart. I would definitely request an improvement to only show the two relevant lines (and the actions applied to them).

However, I'm not sure what to do about the code breaking aspect, since the warning (alongside where clauses in protocol declarations) was intendedly added when type inference had already been cut down. I think we should forward this question to @Slava_Pestov.

There's a bug assigned to me to fix associated type inference in this case where the associated type has been constrained to a concrete type. I think it's better to fix the underlying issue than tweak the fixit at this stage.

2 Likes

Is this the same / similar problem as the seemingly incorrect fixit being suggested for derived protocols providing a new typealias to change the default for an associated type?

public protocol Action {
    associatedtype InputType: FlintLoggable = NoInput
    associatedtype PresenterType  = NoPresenter
}

public protocol DismissingUIAction: Action {
}

extension DismissingUIAction {
    typealias InputType = DismissUIInput
    typealias PresenterType = UIViewController
}

The latter two typealiases produce a warning from the compiler:

Typealias overriding associated type 'InputType' from protocol 'Action' is better expressed as same-type constraint on the protocol

This is not correct advice as far as I understand it, because if you apply the fixit it will produce a conditional conformance won't it? This is not what is intended at all, the intention it to have types conforming to DismissingUIAction to not have to alias these types as an appropriate default is provided.

I think this is a case where you want to restate the associated type:

public protocol Action {
    associatedtype InputType: FlintLoggable = NoInput
    associatedtype PresenterType  = NoPresenter
}

public protocol DismissingUIAction: Action {
    associatedtype InputType = DismissUIInput
    associatedtype PresenterType = UIViewController
}
1 Like

Using typealias indicates that your intention is most likely to constrain the associated type to a concrete type aka specialize it, hence the compiler's advice to impose a same-type constraint on the protocol declaration (not a conditional conformance). On the contrary, to specify a default value for an associated type you have to restate it if necessary and assign the default as Slava suggests. Notice you're already using the default value pattern in Action.

2 Likes

Thanks @Slava_Pestov and @anthonylatsis — unfortunately this is not a great solution for what I'm trying to achieve.

The issue is then that you protocol extensions cannot use the associated types for some reason. e.g:

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

This function cannot use InputType and PresenterType and these must be changed to explicit types matching the type aliases, which is not the goal. The goal is to say "for types conforming to DismissingUIAction, these associated types must always have this value" because the default implementations rely on this.

In this particular case, I can substitute them — with the risk a conforming type may override the types and break the design contract here (effectively user error). The trouble is that if we ignore the warning, this all behaves "as expected" though perhaps by chance.

However in another situation this does not seem possible:

public protocol IntentAction: IntentBackgroundAction {
    /// The type of the `INIntent` thatt this action will implement. This is the Xcode-generated response type produced from your
    /// intent definition configuration.
    associatedtype IntentType: FlintIntent
    /// The type of the `INIntentResponse` for the `INIntent`. This is the Xcode-generated response type produced from your
    /// intent definition configuration.
    associatedtype IntentResponseType: FlintIntentResponse

    /// Automatic aliasing of the presenter to the appropriate type for Intents
    typealias PresenterType = IntentResponsePresenter<IntentResponseType>
...
}

In this case if I change the typealias ("a definition") to associatedtype ("a default"), any extensions on this protocol now suffer the same problem but cannot seemingly know the correct types for their function signatures. Here's an example subtype extension:

extension VerifiedActionBinding where ActionType: IntentAction {
    public func perform(intent: ActionType.IntentType, completion: @escaping (ActionType.IntentResponseType) -> Void) -> MappedActionResult {
        let presenter = IntentResponsePresenter(completion: completion)
        return perform(intent: intent, presenter: presenter)
    }
    
    public func perform(intent: ActionType.IntentType, presenter: ActionType.PresenterType) -> MappedActionResult {
     ...
    }

Here we get a compiler error Cannot invoke 'perform' with an argument list of type '(intent: ActionType.IntentType, presenter: IntentResponsePresenter<ActionType.IntentResponseType>)' ... and then we end up with this viral unfolding of the type aliases requirement (perform calls another perform variant, which now no longer knows the correct presenter type).

Obviously I have a gap in my understanding here, but this feels like a very weird situation. Using typealias it all works as I would "intuit", and yet I get warnings. Trying to stop the warnings that offer fixits that break the code, I get into a possibly unsolvable situation because the other protocol extensions on the same type cannot be specific to a given PresenterType.

I appreciate this is hard to follow.

Here's the above extension source: https://github.com/MontanaFlossCo/Flint/blob/master/FlintCore/Siri%20Intents%20and%20Shortcuts/VerifiedActionBinding%2BShortcut%2BExtensions.swift

If the above has perform(intent:, presenter:) change to:

func perform(intent: ActionType.IntentType, presenter: IntentResponsePresenter<ActionType.IntentResponseType>)

then the actual protocol type's non-specialised extensions cannot be called by that code:

...and to recap. If I ignore the warning, all is great :frowning:

It's best to follow the warning if you're trying to constrain the associated type to a concrete type. This implies «for types conforming to DismissingUIAction, these associated types must always have this value». The warning was specifically added to guide developers in using the preferred syntax. The buggy typealias approach used to be a workaround back when where clauses on protocol declarations were not yet supported. Now, it is but a rudiment that better be taken care of.

protocol P {
  associatedtype Assoc
}


// From P1 onwards, Assoc is always Int. 
protocol P1: P where Assoc == Int {}


// In P1, Assoc is Int by default. The default *may* be overriden by 
// a conformance, so you cannot use Assoc as if it were Int within this 
// protocol and its extensions.
protocol P1: P {
  assocaitedtype Assoc = Int
}

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.