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.


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() {}