Godfather
Godfather

Reputation: 4330

Swift Declare a protocol equatable

I have a protocol:

protocol CustomProtocol {
   var title: String { get }
   var subtitle: String { get }
}

Then i have 2 objects, that conform this procotol. And i want to compare them, so i would like to CustomProtocol to be Equatable.

protocol CustomProtocol: Equatable {
   var title: String { get }
   var subtitle: String { get }
    static func ==(lhs: Self, rhs: Self) -> Bool
}

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

But after that change i get "Protocol CustomProtocol can only be used as a generic constraint because it has Self or associated type requeriments. The only way i can think to solve this is to have a third property like a hash that depends on the others and compare this property.

enter image description here

Here you have a sample playground with the actual code.

Upvotes: 5

Views: 4304

Answers (3)

Renan Freitas
Renan Freitas

Reputation: 1

The problem is the rhs parameter is not the same type of the lhs. They just conform to the same protocol.

You could solve that by using a generic type as the second parameter.

exension CustomProtocol {
    static func ==<T: CustomProtocol>(lhs: Self, rhs: T) -> Bool {
        return lhs.title == rhs.title
    }
}

Upvotes: -1

Khawer Khaliq
Khawer Khaliq

Reputation: 766

Because Equatable has Self requirements, it should not be implemented directly on a protocol. Otherwise, the protocol will be unusable as a type.

To implement Equatable at the protocol level yet be able to use the protocol as a type, you can use type erasure.

To demonstrate, I have modified the code given in your playground to build a type eraser.

For a detailed explanation of the approach I have used, check out this post on my blog:

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

Here is the modified code from your playground:

protocol CustomProtocol {
    var title: String { get }
    var subtitle: String { get }
    func isEqualTo(_ other: CustomProtocol) -> Bool
    func asEquatable() -> AnyEquatableCustomProtocol
}

extension CustomProtocol where Self: Equatable {
    func isEqualTo(_ other: CustomProtocol) -> Bool {
        guard let o = other as? Self else { return false }
        return self == o
    }
    func asEquatable() -> AnyEquatableCustomProtocol {
        return AnyEquatableCustomProtocol(self)
    }
}

struct A: CustomProtocol, Equatable {
    var title: String
    var subtitle: String
    static func ==(lhs: A, rhs: A) -> Bool {
        return lhs.title == rhs.title && lhs.subtitle == rhs.subtitle
    }
}

struct B: CustomProtocol, Equatable {
    var title: String
    var subtitle: String
    static func ==(lhs: B, rhs: B) -> Bool {
        return lhs.title == rhs.title && lhs.subtitle == rhs.subtitle
    }
}

struct AnyEquatableCustomProtocol: CustomProtocol, Equatable {
    var title: String { return value.title }
    var subtitle: String { return value.subtitle }
    init(_ value: CustomProtocol) { self.value = value }
    private let value: CustomProtocol
    static func ==(lhs: AnyEquatableCustomProtocol, rhs: AnyEquatableCustomProtocol) -> Bool {
        return lhs.value.isEqualTo(rhs.value)
    }
}

// instances typed as the protocol
let a: CustomProtocol = A(title: "First title", subtitle: "First subtitle")
let b: CustomProtocol = B(title: "First title", subtitle: "First subtitle")
let equalA: CustomProtocol = A(title: "First title", subtitle: "First subtitle")
let unequalA: CustomProtocol = A(title: "Second title", subtitle: "Second subtitle")

// equality tests
print(a.asEquatable() == b.asEquatable())           // prints false
print(a.asEquatable() == equalA.asEquatable())      // prints true
print(a.asEquatable() == unequalA.asEquatable())    // prints false

The point to note is that with this approach the actual == comparisons are delegated to the underlying concrete types, yet we deal only with protocol types to maintain abstraction.

Here I have used the type erased instances only for one comparison. However, since the type eraser conforms to CustomProtocol these instances can be saved and used in any place where a protocol type is expected. Because they conform to Equatable, they can also be used in any place where Equatable conformance is required.

Just for context, this post explains why it is not advisable to try to implement Equatable conformance or even == functions directly on protocols:

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

Hence the type erasure.

Hope this helps.

Upvotes: 3

Palle
Palle

Reputation: 12109

The Equatable protocol has a self constraint to solve the problem that you only should be able to check equality between objects of the same type, not the same protocol. That's why it has a self-requirement. Otherwise you could just say

let a: Equatable = 42
let b: Equatable = "hello"

and a == b would work. This would be bad because you could compare objects of totally unrelated types. The self-requirement makes this a compile time error.

If you want to compare your objects on a protocol basis, just implement the == operator without a self-requirement:

extension CustomProtocol {
    func == (lhs: CustomProtocol, rhs: CustomProtocol) -> Bool {
         return lhs.name == rhs.name
    }
    func != (lhs: CustomProtocol, rhs: CustomProtocol) -> Bool {
         return !(lhs == rhs)
    }
}

Now you can declare instances of your protocol directly with the CustomProtocol type and compare them.

But maybe the protocol is not the right abstraction in this case. Maybe you should implement this as an abstract class.

Upvotes: 2

Related Questions