CodingMeSwiftly
CodingMeSwiftly

Reputation: 3261

Swift enum multiple generic types involving depending protocols

protocol ProtocolA {
    func someFunc() -> Any
}

protocol ProtocolB: ProtocolA {
    var someVar: Any { get }
}

enum MyEnum<T: ProtocolA, U: ProtocolB> {
    case A(T)
    case B(U)
}

protocol DataObject {
    ...
}

extension DataObject where Self: ProtocolB {
    func doSomething() {
        let x = MyEnum.B(self)
        /// Compiler error:
        /// Cannot invoke 'B' with an argument list of type '(Self)'
    }
}

I don't understand why the above is giving me an error. Strangely, removing either of the two enum generic constraints, leaving the enum with a single constraint, solves the problem...

Note that ProtocolB extends ProtocolA. Is this not supported when used with generic constraints?


UPDATE

Changing MyEnum to this solves the problem:

enum MyEnum {
    typealias T = ProtocolA
    typealias U = ProtocolB

    case A(T)
    case B(U)
}

However, I still don't quite get why...

Upvotes: 1

Views: 1101

Answers (1)

Rob Napier
Rob Napier

Reputation: 299275

The error is a little misleading. Let's simplify the problem to explore it.

protocol ProtocolA {}

protocol ProtocolB: ProtocolA {}

enum MyEnum<T: ProtocolA, U: ProtocolB> {
    case A(T)
    case B(U)
}

struct AThing: ProtocolA {}

struct BThing: ProtocolB {}

let bThing = BThing()
let thing = MyEnum.B(bThing) // error: cannot invoke 'B' with an argument list of type '(BThing)'

So now we have the same problem without needing DataObject. Why does this fail?

MyEnum is generic. That means that in order to create a concrete type, it must know the types of T and U. You provided constraints on those types (one conforms to ProtocolA and the other to ProtocolB), but you didn't say specifically what types they are.

When we get to this line:

let thing = MyEnum.B(bThing)

What type is thing? With type inference, we can work out what U is, but what is T?

let thing: MyEnum<?, BThing> = MyEnum.B(bThing)

There's just not enough context here to work it out, so we have to tell the compiler explicitly:

let thing = MyEnum<AThing, BThing>.B(bThing)

So the full type of thing is MyEnum<AThing, BThing>, which is a different type than MyEnum<OtherAThing, BThing>. (Exactly like [Int] is different type than [String], which is why let xs = [] won't compile without an explicit type definition.)

Your second example that works is not generic, so there's no problem. It simplifies to:

enum MyEnum {
    case A(ProtocolA)
    case B(ProtocolB)
}

Bound typealiases just rename types, they don't create new types (like an unbound typealias would, or as Swift 2.2 calls it, associatedtype). So we know that A takes any ProtocolA and B takes any ProtocolB, and all MyEnum instances have the same type.

The error is unfortunate because the compiler is confused. You'd get a clearer error if you simplified this down to the most basic case. This fails for exactly the same reason as your example.

enum MyEnum<T> {
    case A
}

let x = MyEnum.A // error: generic parameter 'T' could not be inferred

Upvotes: 3

Related Questions