Tim Vermeulen Tim Vermeulen - 3 months ago 21
Swift Question

Generic constraints specific to an enum member

I have a protocol with an associated type:

protocol MyProtocol {
associatedtype Q
}


Now I'd like to have an enum like

enum MyEnum<Q> {
case zero
case one(MyProtocol)
case two(MyProtocol, MyProtocol)
}


where each associated value has
Q
as its associated type. This doesn't work:

enum MyEnum<Q> {
case zero
case one<P: MyProtocol where P.Q == Q>(P)
case two<P1: MyProtocol, P2: MyProtocol where P1.Q == Q, P2.Q == Q>(P1, P2)
}


Apparently, individual enum members can't have their own generic constraints.

The only thing I can think of is to move those constraints to the enum declaration, but this fixates the associated types. To demonstrate why that's not what I want, this is what I'd like to be able to do:

struct StructA: MyProtocol {
typealias Q = Int
}

struct StructB: MyProtocol {
typealias Q = Int
}

var enumValue = MyEnum.one(StructA())
enumValue = .two(StructB(), StructA())
enumValue = .two(StructA(), StructB())


Is there a way around this limitation?

Answer

Type erasure. The answer is always type erasure.

What you need is an AnyProtocol type:

struct AnyProtocol<Element>: MyProtocol {
    typealias Q = Element
    // and the rest of the type-erasure forwarding, based on actual protocol
}

Now you can create an enum that uses them

enum MyEnum<Q> {
    case zero
    case one(AnyProtocol<Q>)
    case two(AnyProtocol<Q>, AnyProtocol<Q>)
}

For a deeper discussion of how to build the type erasers see A Little Respect for AnySequence.

Swift cannot discuss PATs (protocols with associated types) as real types or even abstract types. They can only be constraints. In order to use it as even an abstract type, you have to distill it into a type eraser. Luckily this is quite mechanical and in most cases not difficult. It's so mechanical that eventually the compiler will hopefully do the work for you. But someone has to build the box, and today that's you.