iosswiftexistential-type

How can I conform a Swift enum to `Equatable` when it has an any existential as one of its associated values?


Suppose I have:

protocol MyError: Error, Equatable {
  var errorDispalyTitle: String { get }
  var errorDisplayMessage: String { get }
}

enum ContentState {
  case .loading
  case .error(any MyError)
  case .contentLoaded
}

If i were to implement Equatable in ContentState so I can compare during unit tests I end up with a problem because I have to compare two any MyError types which are boxed and might be of two different underlying types.

extension ContentState: Equatable {
  static func == (lhs: ContentState, rhs: ContentState) -> Bool {
    switch (lhs, rhs) {
    case (.loading, .loading):
      return true
    case (.contentLoaded, .contentLoaded):
      return true
    case (.error(let lhsError), .error(let rhsError)):
      // TODO: have to compare if both underlying types are match and then if they are equal in value
    default:
      return false
    }
  }
}

How do I do this?

I tried lifting the generic from the existential type there to the type (e.g. ContentState<Error: MyError>) which lets it compile when implementing equatable as it knows how to infer the type, but the problem is for whichever class uses that enum it doesnt matter which type is receiving of it, only that is any type of it, and if I don't implement the any existential it starts requiring the generic to be propagated up the chain.


Solution

  • As of Swift 5.7, Swift automatically “opens” an existential when you pass it as an argument of generic type. The implicit self argument can be opened (in fact Swift has always opened the self argument), and Swift can open multiple arguments in a single invocation. So we can write an isEqual(to:) function that compares any Equatable to any other Equatable like this:

    
    extension Equatable {
        func isEqual<B: Equatable>(to b: B) -> Bool {
            return b as? Self == self
        }
    }
    

    And then we can complete your ContentState conformance like this:

    extension ContentState: Equatable {
      static func == (lhs: ContentState, rhs: ContentState) -> Bool {
        switch (lhs, rhs) {
        case (.loading, .loading):
          return true
        case (.contentLoaded, .contentLoaded):
          return true
        case (.error(let lhsError), .error(let rhsError)):
            return lhsError.isEqual(to: rhsError)
        default:
          return false
        }
      }
    }