Swift Equatable on a protocol Swift Equatable on a protocol ios ios

Swift Equatable on a protocol


If you directly implement Equatable on a protocol, it will not longer be usable as a type, which defeats the purpose of using a protocol. Even if you just implement == functions on protocols without Equatable conformance, results can be erroneous. See this post on my blog for a demonstration of these issues:

https://khawerkhaliq.com/blog/swift-protocols-equatable-part-one/

The approach that I have found to work best is to use type erasure. This allows making == comparisons for protocol types (wrapped in type erasers). It is important to note that while we continue to work at the protocol level, the actual == comparisons are delegated to the underlying concrete types to ensure correct results.

I have built a type eraser using your brief example and added some test code at the end. I have added a constant of type String to the protocol and created two conforming types (structs are the easiest for demonstration purposes) to be able to test the various scenarios.

For a detailed explanation of the type erasure methodology used, check out part two of the above blog post:

https://khawerkhaliq.com/blog/swift-protocols-equatable-part-two/

The code below should support the equality comparison that you wanted to implement. You just have to wrap the protocol type in a type eraser instance.

protocol X {    var name: String { get }    func isEqualTo(_ other: X) -> Bool    func asEquatable() -> AnyEquatableX}extension X where Self: Equatable {    func isEqualTo(_ other: X) -> Bool {        guard let otherX = other as? Self else { return false }        return self == otherX    }    func asEquatable() -> AnyEquatableX {        return AnyEquatableX(self)    }}struct Y: X, Equatable {    let name: String    static func ==(lhs: Y, rhs: Y) -> Bool {        return lhs.name == rhs.name    }}struct Z: X, Equatable {    let name: String    static func ==(lhs: Z, rhs: Z) -> Bool {        return lhs.name == rhs.name    }}struct AnyEquatableX: X, Equatable {    var name: String { return value.name }    init(_ value: X) { self.value = value }    private let value: X    static func ==(lhs: AnyEquatableX, rhs: AnyEquatableX) -> Bool {        return lhs.value.isEqualTo(rhs.value)    }}// instances typed as the protocollet y: X = Y(name: "My name")let z: X = Z(name: "My name")let equalY: X = Y(name: "My name")let unequalY: X = Y(name: "Your name")// equality testsprint(y.asEquatable() == z.asEquatable())           // prints falseprint(y.asEquatable() == equalY.asEquatable())      // prints trueprint(y.asEquatable() == unequalY.asEquatable())    // prints false

Note that since the type eraser conforms to the protocol, you can use instances of the type eraser anywhere an instance of the protocol type is expected.

Hope this helps.


The reason why you should think twice about having a protocol conform to Equatable is that in many cases it just doesn't make sense. Consider this example:

protocol Pet: Equatable {  var age: Int { get }}extension Pet {  static func == (lhs: Pet, rhs: Pet) -> Bool {    return lhs.age == rhs.age  }}struct Dog: Pet {  let age: Int  let favoriteFood: String}struct Cat: Pet {  let age: Int  let favoriteLitter: String}let rover: Pet = Dog(age: "1", favoriteFood: "Pizza")let simba: Pet = Cat(age: "1", favoriteLitter: "Purina")if rover == simba {  print("Should this be true??")}

You allude to type checking within the implementation of == but the problem is that you have no information about either of the types beyond them being Pets and you don't know all the things that might be a Pet (maybe you will add a Bird and Rabbit later). If you really need this, another approach can be modeling how languages like C# implement equality, by doing something like:

protocol IsEqual {  func isEqualTo(_ object: Any) -> Bool}protocol Pet: IsEqual {  var age: Int { get }}struct Dog: Pet {  let age: Int  let favoriteFood: String  func isEqualTo(_ object: Any) -> Bool {    guard let otherDog = object as? Dog else { return false }    return age == otherDog.age && favoriteFood == otherDog.favoriteFood  }}struct Cat: Pet {  let age: Int  let favoriteLitter: String  func isEqualTo(_ object: Any) -> Bool {    guard let otherCat = object as? Cat else { return false }    return age == otherCat.age && favoriteLitter == otherCat.favoriteLitter  }}let rover: Pet = Dog(age: "1", favoriteFood: "Pizza")let simba: Pet = Cat(age: "1", favoriteLitter: "Purina")if !rover.isEqualTo(simba) {  print("That's more like it.")}

At which point if you really wanted, you could implement == without implementing Equatable:

static func == (lhs: IsEqual, rhs: IsEqual) -> Bool { return lhs.isEqualTo(rhs) }

One thing you would have to watch out for in this case is inheritance though. Because you could downcast an inheriting type and erase the information that might make isEqualTo not make logical sense.

The best way to go though is to only implement equality on the class/struct themselves and use another mechanism for type checking.


Determining equality across conformances to a Swift protocol is possible without type erasure if:

  • you are willing to forgo the operator syntax (i.e. call isEqual(to:) instead of ==)
  • you control the protocol (so you can add an isEqual(to:) func to it)
import XCTestprotocol Shape {    func isEqual (to: Shape) -> Bool}extension Shape where Self : Equatable {    func isEqual (to: Shape) -> Bool {        return (to as? Self).flatMap({ $0 == self }) ?? false    }}struct Circle : Shape, Equatable {    let radius: Double}struct Square : Shape, Equatable {    let edge: Double}class ProtocolConformanceEquality: XCTestCase {    func test() {        // Does the right thing for same type        XCTAssertTrue(Circle(radius: 1).isEqual(to: Circle(radius: 1)))        XCTAssertFalse(Circle(radius: 1).isEqual(to: Circle(radius: 2)))        // Does the right thing for different types        XCTAssertFalse(Square(edge: 1).isEqual(to: Circle(radius: 1)))    }}

Any conformances don't conform to Equatable will need to implement isEqual(to:) themselves