Swift Equatable on a protocol
Asked Answered
D

10

78

I don't think this can be done but I'll ask anyway. I have a protocol:

protocol X {}

And a class:

class Y:X {}

In the rest of my code I refer to everything using the protocol X. In that code I would like to be able to do something like:

let a:X = ...
let b:X = ...
if a == b {...}

The problem is that if I try to implement Equatable:

protocol X: Equatable {}
func ==(lhs:X, rhs:X) -> Bool {
    if let l = lhs as? Y, let r = hrs as? Y {
        return l.something == r.something
    }
    return false
} 

The idea to try and allow the use of == whilst hiding the implementations behind the protocol.

Swift doesn't like this though because Equatable has Self references and it will no longer allow me to use it as a type. Only as a generic argument.

So has anyone found a way to apply an operator to a protocol without the protocol becoming unusable as a type?

Deutoplasm answered 9/2, 2017 at 6:55 Comment(0)
I
63

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 protocol
let 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 tests
print(y.asEquatable() == z.asEquatable())           // prints false
print(y.asEquatable() == equalY.asEquatable())      // prints true
print(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.

Internalcombustion answered 12/10, 2017 at 21:17 Comment(0)
H
23

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.

Harrelson answered 9/2, 2017 at 9:48 Comment(2)
Thanks. I've been considering the isEqual option (from Java) but was hoping to keep it simple because I have a case where I have different classes that represent the same contextural thing and therefore I would them to be regarded as equal even though they are different implementations.Deutoplasm
Implementing equality on two different types of objects is a slippery slope to get on. I'd recommend another option to compare them, perhaps by transforming one type into the other one and then comparing them.Harrelson
I
21

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 XCTest

protocol 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

Impresa answered 20/1, 2020 at 6:36 Comment(1)
This is the best answer as of Swift 5.1 – would be better if provided with another example such that isEqual(to:) would provide a default implementation to other protocols other than ShapeMumps
C
13

maybe this will be helpful for you:

protocol X:Equatable {
    var name: String {get set}

}

extension X {
    static func ==(lhs: Self, rhs: Self) -> Bool {
        return lhs.name == rhs.name
    }
}

struct Test : X {
    var name: String
}

let first = Test(name: "Test1")
let second = Test(name: "Test2")

print(first == second) // false
Clambake answered 9/2, 2017 at 8:15 Comment(1)
Thanks, but this works until you try to do something like `let first = Test(name: "Test1") as X' then you get the error about not allowing 'X' as a type. My problem is that my classes are deep within an API and only expose themselves via the protocol. So the usage of those instances must be of the protocol type.Deutoplasm
C
5

Not sure why you need all instances of your protocol to conform to Equatable, but I prefer letting classes implement their equality methods.

In this case, I'd leave the protocol simple:

protocol MyProtocol {
    func doSomething()
}

If you require that an object that conforms to MyProtocol is also Equatable you can use MyProtocol & Equatable as type constraint:

// Equivalent: func doSomething<T>(element1: T, element2: T) where T: MyProtocol & Equatable {
func doSomething<T: MyProtocol & Equatable>(element1: T, element2: T) {
    if element1 == element2 {
        element1.doSomething()
    }
}

This way you can keep your specification clear and let subclasses implement their equality method only if required.

Cockleshell answered 9/2, 2017 at 11:28 Comment(0)
C
5

All people who say that you can't implement Equatable for a protocol just don't try hard enough. Here is the solution (Swift 4.1) for your protocol X example:

protocol X: Equatable {
    var something: Int { get }
}

// Define this operator in the global scope!
func ==<L: X, R: X>(l: L, r: R) -> Bool {
    return l.something == r.something
}

And it works!

class Y: X {
    var something: Int = 14
}

struct Z: X {
    let something: Int = 9
}

let y = Y()
let z = Z()
print(y == z) // false

y.something = z.something
print(y == z) // true

The only problem is that you can't write let a: X = Y() because of "Protocol can only be used as a generic constraint" error.

Counselor answered 8/9, 2018 at 13:30 Comment(6)
This should have more upvotes, solid solutionDaudet
That's pretty cool. One gotcha to point out is that it doesn't compare types, by design. So if you were to have struct Square: Shape { let sides = 4 } and struct Rhombus: Shape { let sides = 4 } then Square() == Rhombus() is true. Perhaps an easy fix to also check the type is to change the return from the == operator to be: return (r as? L)?.something == r.somethingVariscite
@LouZell, interesting. Did you check if r as? L works?Counselor
Yup! It worked for my limited case (where I need to differentiate between two conformers of a protocol, e.g. Rhombus != Square in the example above)Variscite
This is the solid solution. It definitely must have more upvotesContexture
As stated in some other answers, this works only if you don't use X as a type, the moment you want to do so you would get again the problem: "Use of protocol 'X' as a type must be written 'any X'"Dionne
H
3

I would still advise against implementing == using polymorphism. It's a bit of a code smell. If you want to give the framework user something he can test equality with then you should really be vending a struct, not a protocol. That's not to say that it can't be the protocols that are vending the structs though:

struct Info: Equatable {
  let a: Int
  let b: String

  static func == (lhs: Info, rhs: Info) -> Bool {
    return lhs.a == rhs.a && lhs.b == rhs.b
  }
}

protocol HasInfo {
  var info: Info { get }
}

class FirstClass: HasInfo {
  /* ... */
}

class SecondClass: HasInfo {
  /* ... */
}

let x: HasInfo = FirstClass( /* ... */ )
let y: HasInfo = SecondClass( /* ... */ )

print(x == y) // nope
print(x.info == y.info) // yep

I think this more efficiently communicates your intent, which is basically "you have these things and you don't know if they are the same things, but you do know they have the same set of properties and you can test whether those properties are the same." That is pretty close to how I would implement that Money example.

Harrelson answered 11/2, 2017 at 4:16 Comment(0)
W
2

You have to implement a protocol extension constrained to your class type. Inside that extension you should implement the Equatable operator.

public protocol Protocolable: class, Equatable
{
    // Other stuff here...
}

public extension Protocolable where Self: TheClass
{
    public static func ==(lhs: Self, rhs:Self) -> Bool 
    {
        return lhs.name == rhs.name
    } 
}


public class TheClass: Protocolable
{
    public var name: String

    public init(named name: String)
    {
        self.name = name
    }
}

let aClass: TheClass = TheClass(named: "Cars")
let otherClass: TheClass = TheClass(named: "Wall-E")

if aClass == otherClass
{
    print("Equals")
}
else
{
    print("Non Equals")
}

But let me recommend you add the operator implementation to your class. Keep It Simple ;-)

Wellesley answered 9/2, 2017 at 7:15 Comment(1)
Thanks. I"m trying to avoid constraining a protocol extension to a class because (and perhaps this is a bad idea but it works for my case) I have different implementations of the protocol which in effect refer to the same thing, therefore I would like them to be regarded as equal.Deutoplasm
W
1

I came cross this same issue and I figured that the == operator can be implemented in the global scope (as it used to be), as opposed to a static func inside the protocol's scope:

// This should go in the global scope

public func == (lhs: MyProtocol?, rhs: MyProtocol?) -> Bool { return lhs?.id == rhs?.id }
public func != (lhs: MyProtocol?, rhs: MyProtocol?) -> Bool { return lhs?.id != rhs?.id }

Note that if you use linters such as SwiftLint's static_operator, you'll have to wrap that code around // swiftlint:disable static_operator to silent linter warnings.

Then this code will start compiling:

let obj1: MyProtocol = ConcreteType(id: "1")
let obj2: MyProtocol = ConcreteType(id: "2")
if obj1 == obj2 {
    print("They're equal.")
} else {
    print("They're not equal.")
}
Waterfront answered 18/1, 2021 at 15:50 Comment(0)
C
0

took some of the code from above and came with the following sollution.

it uses the IsEqual protocol instead of the Equatable protocol and with a few line codes you will be able to compare any two protocol objects with each other, wether they are optional or not, are in an array and even add comparing dates while I was at it.

protocol IsEqual {
    func isEqualTo(_ object: Any) -> Bool
}

func == (lhs: IsEqual?, rhs: IsEqual?) -> Bool {
    guard let lhs = lhs else { return rhs == nil }
    guard let rhs = rhs else { return false }
    return lhs.isEqualTo(rhs) }

func == (lhs: [IsEqual]?, rhs: [IsEqual]?) -> Bool {
    guard let lhs = lhs else { return rhs == nil }
    guard let rhs = rhs else { return false }
    
    guard lhs.count == rhs.count else { return false }
    for i in 0..<lhs.count {
        if !lhs[i].isEqualTo(rhs[i]) {
            return false
        }
    }
    return true
}

func == (lhs: Date?, rhs: Date?) -> Bool {
    guard let lhs = lhs else { return rhs == nil }
    guard let rhs = rhs else { return false }
    
    return lhs.compare(rhs) == .orderedSame
}

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
  }
}
Crankshaft answered 10/11, 2021 at 10:29 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.