Conditional conformance of all types that conform to another protocol

I’m trying to do this thing and I can’t figure out how to “spell” it in Swift.

I have a protocol—Die—that defines the behavior of a die for random elements, but it’s got an associated type to keep the values generic, so it can be for dice with faces that are numbers like a standard six-sided die or for D&D style polyhedrals, but it also could represent a die with values that are something else entirely, like colors.

I have another protocol—NumericDie—that’s for dice that have a value that’s numeric. I’d like to create conditional conformance (kind of like how Array conforms to Equatable if it’s Element is Equatable, etc.)

I want types that conform to Die and have Values (the Associated Type) which conform to RawRepresentable where the RawValue conforms to Numeric to automatically gain conformance to NumericDie.

So for example,

struct D6: Die {
 //assume the conformance to Die is implemented 

 enum Values: Int {
  case none = 0
  case one, two, three, four, five, six
 }
}

So what can I write to make D6 (and other number-based dice) automatically conform to NumericDie?

2 Likes

This is discussed in the generics manifesto: https://github.com/apple/swift/blob/main/docs/GenericsManifesto.md#retroactive-protocol-refinement.

3 Likes

Consider this for an example of how to use conforming protocols, associated types, and default implementations, where any "Die" conformant needs only to have the collection of possible values defined (though note the default implementation assumes & requires that the values collection be non-empty)

protocol Die {
    associatedtype Values: Collection
    static var values: Values { get }
    static func rollDie() -> Values.Element
}

extension Die {
    static func rollDie() -> Values.Element { values.randomElement().unsafelyUnwrapped }
}

protocol NumericDie: Die {
    static func rollDie() -> Int
    static var values: ClosedRange<Int> { get }
}

struct D6: NumericDie {
    static let values = (1 ... 6)
}

struct DElemental: Die {
    static let values = ["Earth", "Fire", "Air", "Water"]
}

print(D6.rollDie())              // "3"
print(DElemental.rollDie())      // "Air"

Sidenote - my example above is not super clean in this case in large part because it doesn't necessarily make sense for a variation of a die to be an actual Struct (or Class or Enum). If instead one removes all the static definitions from the protocols, one can instantiate individual dice, like this:

struct GenericDie<T>: Die {
    let values: [T]
}

extension GenericDie: NumericDie where T == Int {}

extension Array where Element: NumericDie {
    func rollDice() -> Int { reduce(into: 0) { $0 += $1.rollDie() } }
}

let d6 = GenericDie(values: Array((1...6)))
let d20 = GenericDie(values: Array((1...20)))
let dElemental = GenericDie(values: ["Earth", "Fire", "Air", "Water"])

print(dElemental.rollDie())
// "Water"
print([d6, d20, d20].rollDice())
// 28
2 Likes

Yes! This was the missing piece I needed. I just need to make an actual type that’s generic and extend THAT with the conditional conformance. Thank you!

1 Like