Alexito's World

A world of coding 💻, by Alejandro Martinez

Synthesized Comparable conformance for enums

Swift 5.3 comes with a nice addition for enums: it will synthesize the required implementation to conform to the Comparable protocol. This reduces a lot the boilerplate needed to make your enums comparable.

Subscribe to my channel to be notified when new videos about Swift Evolution are released.

Up until now there were multiple ways of adopting conformance to this protocol, each one with different degrees of inconvenience.

Even if Swift 5.3 solves all of them, it's still useful to know these techniques for the rare cases when you want to implement the conformance manually in order to customise some behaviour.

Comparable Raw Values

The first trick and the one that requires less code is to bake your enum with a comparable raw value, like an Int.

enum Priority: Int, Comparable {
    case low
    case medium
    case high

Having access to an integer makes it really easy to implement the comparison function.

     static func < (lhs: Self, rhs: Self) -> Bool {
       return lhs.rawValue < rhs.rawValue
   }
}

The problem with this is that now the users of your type will be able to treat it like an integer by accessing its raw value. This is something that you rarely want and it should be avoided.

Private Raw Value

The alternative is to keep the raw value private. This can't be done automatically so it adds some boilerplate.

private var comparisonValue: Int {
    switch self {
    case .low:
        return 0
    case .medium:
        return 1
    case .high:
        return 2
    }
}

static func < (lhs: Self, rhs: Self) -> Bool {
    return lhs.comparisonValue < rhs.comparisonValue
}

This lets you keep the comparison function simple and without opening the type to misuse.

Hardcore comparison

Another alternative, and probably the most correct one, is to implement the comparison without using any proxy value, by just checking the cases themselves.

private static func minimum(_ lhs: Self, _ rhs: Self) -> Self {
    switch (lhs, rhs) {
    case (.low, _), (_, .low):
        return .low
    case (.medium, _), (_, .medium):
        return .medium
    case (.high, _), (_, .high):
        return .high
    }
}

static func < (lhs: Self, rhs: Self) -> Bool {
    return (lhs != rhs) && (lhs == Self.minimum(lhs, rhs))
}

Conclusion

As you can see the automatic synthesis of Swift 5.3 it's very welcomed. Without it we can't avoid adding some boilerplate and even hurting the API of our type. You also have to consider that if your enum has associated values it adds another layer of complexity.

So use Swift 5.3 features when possible, but keep these techniques on your toolbelt for those rare cases where they may be necessary.