Static constraint for rock-paper-scissors protocol relationship

Is there a way to statically guarantee a rock-paper-scissors relationship using protocols such that a type can't beat itself?

For example, we could define the protocol relationship as:

protocol ThreeCycle {
    associatedtype Beats: ThreeCycle where Beats.Beats.Beats == Self
}

But then we're allowed to compile a type which can beat itself directly:

struct PowerfulRock: ThreeCycle {
    typealias Beats = PowerfulRock
}

Can we disallow the associatedtype Beats from equaling Self?

Alternatively we could split the types into three protocols:

protocol RockType {
    associatedtype Beats: ScissorType where Beats.Beats.Beats == Self
}
protocol ScissorType {
    associatedtype Beats: PaperType where Beats.Beats.Beats == Self
}
protocol PaperType {
    associatedtype Beats: RockType where Beats.Beats.Beats == Self
}

But again we're allowed to compile a type which can beat itself directly:

struct PowerfulRock2: RockType, ScissorType, PaperType {
    typealias Beats = PowerfulRock2
}

In this case, can we disallow a type from conforming to a specific combination of protocols?

No. Since we allow retroactive conformances I don't think this would be possible in general.

But the actual problem seems pretty straightforward with three protocols:

associatedtype Next: Paper
associatedtype Next: Scissors
associatedtype Next: Rock

Thanks for the response Slava. What stops the compiler from checking if a type is not allowed to retroactively conform to a protocol? For example, we provide the constraint that any type conforming to protocol A can't also conform to protocol B. If we create a type T: A and extend it later to conform to B, why can't the compiler raise an error at that point?

How about this. You can take advantage of the fact that a type can only (non-retroactively) conform to a protocol once with one associated type binding to do something like this

protocol ThreeCycle {
    associatedtype Beats: ThreeCycle where Beats.Beats.Beats == Self
    associatedtype BeatMarker
}

protocol RockType: ThreeCycle where Beats: ScissorType, BeatMarker == any ScissorType { }
protocol ScissorType: ThreeCycle where Beats: PaperType, BeatMarker == any PaperType { }
protocol PaperType: ThreeCycle where Beats: RockType, BeatMarker == any RockType { }

The same-type constraints on the BeatMarker associated type make it impossible for one conformance to satisfy more than one of the RockType, ScissorType, or PaperType protocol requirements.

8 Likes

Thanks for your response Joe! That's a great answer. It also gives a way to create an XOR between protocols

protocol XOR {
    associatedtype Marker
}

protocol A: XOR where Marker == any A {
    func doThis()
}

protocol B: XOR where Marker == any B {
    func doThat()
}

struct Both: A, B { //Type 'Both' does not conform to protocol 'A', 'B' or 'Exclude'
    func doThis() {}
    func doThat() {}
}