6
votes

I have several different enums in my project, that conform to the same protocol. The compareEnumType method from the protocol compares enum cases ignoring associated values. Here is my code from playground:

protocol EquatableEnumType {
    static func compareEnumType(lhs: Self, rhs: Self) -> Bool
}

enum MyEnum: EquatableEnumType {
    case A(Int)
    case B

    static func compareEnumType(lhs: MyEnum, rhs: MyEnum) -> Bool {
        switch (lhs, rhs) {
        case (.A, .A): return true
        case (.B, .B): return true
        default: return false
        }
    }
}

enum MyEnum2: EquatableEnumType {
    case X(String)
    case Y

    static func compareEnumType(lhs: MyEnum2, rhs: MyEnum2) -> Bool {
        switch (lhs, rhs) {
        case (.X, .X): return true
        case (.Y, .Y): return true
        default: return false
        }
    }
}

let a = MyEnum.A(5)
let a1 = MyEnum.A(3)
if MyEnum.compareEnumType(lhs: a, rhs: a1) {
    print("equal") // -> true, prints "equal"
}

let x = MyEnum2.X("table")
let x1 = MyEnum2.X("chair")
if MyEnum2.compareEnumType(lhs: x, rhs: x1) {
    print("equal2") // -> true, prints "equal2"
}

In my real project I have more than 2 enums, and for each of them I have to have similar implementation of compareEnumType function.

The question is: is it possible to have a generic implementation of compareEnumType which would work for all enums conforming to EquatableEnumType protocol?

I tried to write a default implementation in protocol extension like this:

extension EquatableEnumType {
    static func compareEnumType(lhs: Self, rhs: Self) -> Bool {
        // how to implement???
    }
}

But I'm stuck with implementation. I don't see a way to access a value contained in lhs and rhs. Could anyone help me?

2
This violates the meaning of Equatable, which "implies substitutability—any two instances that compare equally can be used interchangeably in any code that depends on their values. To maintain substitutability, the == operator should take into account all visible aspects of an Equatable type. Exposing nonvalue aspects of Equatable types other than class identity is discouraged, and any that are exposed should be explicitly pointed out in documentation." This is a very bad idea, and you should redesign your types to avoid it. If you are determined to do this, the tool you'll need is SwiftGen. - Rob Napier
If your enums are carrying private information that is not relevant to equality (such as caches), then you should almost certainly be using classes or structs here instead. Enums are not well suited to that problem. (Any enum-based solution can be converted into a protocol+struct-based equivalent, and vice versa. The maintenance trade-offs will be different, but the logic can always be implemented because they're duals.) - Rob Napier
@RobNapier I don't want to change a meaning or implementation of ==. I know that it takes into account associated values and that's correct. In my protocol I want to compare enum cases only, ignoring associated values. I tried using == and it doesn't work for me (which is correct). I'm looking for another solution for how I can compare enum cases ignoring associated values in generic way. - Anastasia
Sure. Sourcery. github.com/krzysztofzablocki/Sourcery (sorry for saying SwiftGen before; that's the lower-level library that Sourcery is based on, but for this you want Sourcery) The short and long answer is that you need a level of metaprogramming that doesn't exist in Swift, and that's where Sourcery fills the gap for now. Some day there will be more metaprogramming in Swift so that Sourcery is unnecessary, but today is not that day. - Rob Napier
(And with your edits, there's nothing wrong with this and certainly makes sense. Only tying it into Equatable was a problem.) - Rob Napier

2 Answers

2
votes

Easy! I would use an instance method, but you can rewrite it to a class function, if you really need it to be static.

extension EquatableEnumCase {
    func isSameCase(as other: Self) -> Bool {
        let mirrorSelf = Mirror(reflecting: self)
        let mirrorOther = Mirror(reflecting: other)
        if let caseSelf = mirrorSelf.children.first?.label, let caseOther = mirrorOther.children.first?.label {
            return (caseSelf == caseOther) //Avoid nil comparation, because (nil == nil) returns true
        } else { return false}
    }
} 


enum MyEnum1: EquatableEnumCase {
    case A(Int)
    case B
}

enum MyEnum2: EquatableEnumCase {
    case X(String)
    case Y
}

let a = MyEnum1.A(5)
let a1 = MyEnum1.A(3)
if a.isSameCase(as: a1) {
    print("equal") // -> true, prints "equal1"
}

let x = MyEnum2.X("table")
let x1 = MyEnum2.X("chair")

if x.isSameCase(as: x1) {
    print("equal2") // -> true, prints "equal2"
}

let y = MyEnum2.Y
print(x.isSameCase(as: y) ? "equal3": "not equal3") // -> false, "not equal3"
0
votes

I don't think that you can autogenerate this so here is a way using extensions. I suggest to create a new enum CompareEnumMethod which tells if you want to compare the associated values or only the type. Create a new function compareEnum in your protocol that has this enum as a parameter. Then, you can create an extension for ==(lhs:,rhs:) and say that it uses .value and for compareEnumType you use .type. Only the compareEnum method has to be implemented in each enum now.

enum CompareEnumMethod {
    case type, value
}

protocol EquatableEnumType: Equatable {
    static func compareEnumType(lhs: Self, rhs: Self) -> Bool
    static func compareEnum(lhs: Self, rhs: Self, method: CompareEnumMethod) -> Bool
}

extension EquatableEnumType {
    static func compareEnumType(lhs: Self, rhs: Self) -> Bool {
        return Self.compareEnum(lhs: lhs, rhs: rhs, method: .type)
    }

    static func ==(lhs: Self, rhs: Self) -> Bool {
        return Self.compareEnum(lhs: lhs, rhs: rhs, method: .value)
    }
}

enum MyEnum: EquatableEnumType {
    case A(Int)
    case B

    static func compareEnum(lhs: MyEnum, rhs: MyEnum, method: CompareEnumMethod) -> Bool {
        switch (lhs, rhs, method) {
        case let (.A(lhsA), .A(rhsA), .value):
            return lhsA == rhsA
        case (.A, .A, .type),
             (.B, .B, _):
            return true
        default:
            return false
        }
    }
}

let a0 = MyEnum.A(5)
let a1 = MyEnum.A(3)
let b0 = MyEnum.B
print(MyEnum.compareEnumType(lhs: a0, rhs: a1)) //true
print(a0 == a1) //false
print(MyEnum.compareEnumType(lhs: a0, rhs: b0)) //false