How to compare two arrays of protocols for equality in Swift?
Asked Answered
A

2

9

I've run into a situation that I'm sure is not that uncommon. I have two arrays of objects that conform to a protocol and I want to check if they are the equal.

What I'd really like to do is this:

protocol Pattern: Equatable
{
    func isEqualTo(other: Pattern) -> Bool
}

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

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

However, this leads to the compile error:

Error:(10, 30) protocol 'Pattern' can only be used as a generic constraint because it has Self or associated type requirements

Based on this post I realise that I need to lose the Equatable inheritance on my protocol and push it down onto the concrete 'Pattern' declarations. Though I really don't understand why. If I'm defining how the two objects are equal based on the protocol by overloading == there really is no issue as far as I can see. I don't even need to know the actual types or whether they are classes or structs.

Regardless, this is all well and good and I can now compare concretePattern.isEqualTo(otherConcretePattern) but the issue remains that I can no longer compare arrays of these objects like I can compare an array of a concrete type as array equality relies on overloading the == operator.

The best I've managed to do so far is glom an isEqualTo method onto CollectionType via an extension. This at least allows me to compare arrays. But frankly, this code stinks.

extension CollectionType where Generator.Element == Pattern
{
    func isEqualTo(patterns:[Pattern]) -> Bool {
        return self.count as? Int == patterns.count && !zip(self, patterns).contains { !$0.isEqualTo($1) }
    }
}

Is there really no other way of doing this? Please tell me I'm missing something obvious.

Astrophotography answered 15/10, 2015 at 11:15 Comment(0)
H
7

I have two arrays of objects that conform to a protocol and I want to check if they are the equal.

So you want to say the two arrays are equal if all the elements in them are equal and the elements all conform to pattern. i.e.

If a, b, c and d are all things that conform to Pattern, you want

a == c 
a != b
a != d
b != d

let array1: [Pattern] = [a, b, c]
let array2: [Pattern] = [a, b, a]
let array3: [Pattern] = [a, d, c]

array1 == array2  // true
array1 == array3  // false

The easiest way to do this is actually to define an equality operator for two arrays of patterns i.e.

protocol Pattern
{
    func isEqualTo(other: Pattern) -> Bool
}

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

func ==(lhs: [Pattern], rhs: [Pattern]) -> Bool
{
    guard lhs.count == rhs.count else { return false }
    var i1 = lhs.generate()
    var i2 = rhs.generate()
    var isEqual = true
    while let e1 = i1.next(), e2 = i2.next() where isEqual
    {
        isEqual = e1 == e2
    }
    return isEqual
}

I defined two types that conform to Pattern and tried various equality compares and it all works

struct Foo: Pattern
{
    let data: String
    init(data: String)
    {
        self.data = data
    }
    func isEqualTo(other: Pattern) -> Bool
    {
        guard let other = other as? Foo else { return false }
        return self.data == other.data
    }
}

struct Bar: Pattern
{
    let data: String
    init(data: String)
    {
        self.data = data
    }
    func isEqualTo(other: Pattern) -> Bool
    {
        guard let other = other as? Bar else { return false }
        return self.data == other.data
    }
}

let a = Foo(data: "jeremyp")
let b = Bar(data: "jeremyp")
let c = Foo(data: "jeremyp")
let d = Foo(data: "jeremy")

let comp1 = a == c // true
let comp2 = a == b // false
let comp3 = a == d // false

let array1: [Pattern] = [a, b, c]
let array2: [Pattern] = [a, b, a]
let array3: [Pattern] = [a, d, c]

let comp4 = array1 == array2 // true
let comp5 = array1 == array3 // false
Herwick answered 15/10, 2015 at 15:21 Comment(4)
So the answer is to just not make the Pattern protocol inherit from Equatable?Astrophotography
I tried lots of ways and this turned out to be the easiest. If Pattern is not Equatable you can't compare arrays of Pattern so you have to create your own equality operator for the array.Herwick
I had actually done this at one point before I trying to inherit from Equatable. I'm not sure why I forgot. Thanks for the help.Astrophotography
Swift is warning me about == function with arrays. Member operator '==' of protocol must have at least one argument of type Self. Any idea?Deep
M
2

The Swift answer:

protocol _Pattern
{
    func _isEqualTo(_other: Any) -> Bool?
}

extension _Pattern where Self: Pattern
{
    func _isEqualTo(_other: Any) -> Bool?
    {
        return (_other as? Self).map({ self.isEqualTo($0) })
    }
}

protocol Pattern: _Pattern, Equatable
{
    func isEqualTo(other: Self) -> Bool
}

extension Pattern
{
    func isEqualTo(other: _Pattern) -> Bool
    {
        return _isEqualTo(other) ?? false
    }
}

func == <T: Pattern>(rhs: T, lhs: T) -> Bool
{
    return rhs.isEqualTo(lhs)
}

This is a pattern (pun intended) that I developed myself, and it works extremely well for situations like these. _Pattern is an auto-implemented protocol courtesy of the new protocol-extension feature, and represents a type-erased version of Pattern.

Margarettamargarette answered 15/10, 2015 at 11:22 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.