2

The following code compiled without issue

protocol Animal {
}

var animals = [Animal]()

However, we have a new requirement, where we need to compare array of Animal

protocol Animal {
}

func x(a0: [Animal], a1: [Animal]) -> Bool {
    return a0 == a1
}

var animals = [Animal]()

The above code will yield compilation error

protocol 'Animal' as a type cannot conform to 'Equatable'


We tend to fix by

protocol Animal: Equatable {
}

func x(a0: [Animal], a1: [Animal]) -> Bool {
    return a0 == a1
}

var animals = [Animal]()

At array declaration line, we are getting error

protocol 'Animal' can only be used as a generic constraint because it has Self or associated type requirements

May I know,

  1. Why we can have an array of protocol, before the protocol is conforming to Equatable?
  2. Why we are not allowed to have an array of protocol, once the protocol is conforming to Equatable?
  3. What are some good ways to fix such error?
Cheok Yan Cheng
  • 47,586
  • 132
  • 466
  • 875
  • 1
    This cannot work. It's impossible to compare protocols because basically any type can adopt the protocol. Imagine you have a type `Cat : Animal` and `Dog: Animal`. How you do compare the types? – vadian Jan 14 '22 at 18:37
  • 2
    An array of a protocol-type is _always_ a bad idea, whether its a generic protocol or not. See https://stackoverflow.com/questions/33112559/protocol-doesnt-conform-to-itself If you really had to do this, you would need to use _type erasure_ (i.e. you'd need an equatable AnyAnimal type). Apple has a WWDC video about this: https://developer.apple.com/videos/play/wwdc2015/408/ – matt Jan 14 '22 at 19:00
  • I still don't get it. Before conforming to Equatable, I am allowed to have array of protocol. But, why once conforming to Equatable, I am prohibit from doing so? Why do you think array of protocol-type is a bad idea? I thought most main stream languages like Java, allow and welcome array of interface (protocol). – Cheok Yan Cheng Jan 14 '22 at 19:09
  • You are allowed to make an array of protocol but that doesn't make it a good idea. – matt Jan 14 '22 at 19:26
  • @CheokYanCheng Java has its `isEqual()` designed differently. It's defined on `Object` (the language's universal supertype), and expects a parameter of type `Object`. I.e., any two objects of any type can be compared. It's up to the implementer of `isEqual()` to write something like `if (this.class != that.class) return false`. You can contrast this with C#'s `IEquatable`, where `Equal(T? other)` requires the compared object to be a `T` (or `null`)... – Alexander Jan 14 '22 at 19:30
  • You can see an example of that in `class Person : IEquatable` ([here](https://learn.microsoft.com/en-us/dotnet/api/system.iequatable-1.equals?view=net-6.0#system-iequatable-1-equals(-0))). The equals method ends up having the signature `bool Equals(Person? other)`, which is basically the same as the Self-requirement in Swift.Equatable. If you want generalized equality according to some custom notion (about Animals or whatever), you need to make your own business-logic-specific equality type, that *isn't* `Swift.Equatable`. – Alexander Jan 14 '22 at 19:32
  • 2
    Our own Rob Napier made a great talk that covers exactly this, specifically about Equatable: https://youtu.be/_m6DxTEisR8?t=2538 – Alexander Jan 14 '22 at 19:35
  • @Alexander Thanks for the video. This is my life saver! - https://youtu.be/_m6DxTEisR8?t=2585 – Cheok Yan Cheng Jan 15 '22 at 04:52

3 Answers3

1

This part of Swift can be a little confusing, and there are plans to improve it.

When you write something like a0: [Animal], you saying that your function takes an array argument, whose elements are protocol existentials (of the Animal protocol).

An existential Animal is an object that gives its user uniform access to all the requirements (properties, methods, subscripts, initializers, etc.) of the Animal protocol, regardless of the concrete type of the underlying conforming object (Cat, Dog, etc.).

In the new world post SE-0335, you code would have to be spelled like this:

func x(a0: [any Animal], a1: [any Animal]) -> Bool {
    return a0 == a1
}

The issue becomes more clear: there's no guarantee that a0 and a1 contain animals of the same type. It's now literally written in the code: they're arrays of any animal type. Each one can contain animals of any type, and there's no relationship between the types of the animals in a0 vs in a1. This is an issue because Equatable is verify specific about its requirements: its == operator is only applicable to two objects of the same type.

To remedy this, you would need to make your function generic, to constain a0 and a1 to contain objects of some particular type:

func x<A: Animal>(a0: [A], a1: [A]) -> Bool {
    return a0 == a1
}
Alexander
  • 59,041
  • 12
  • 98
  • 151
0

Protocol can't conform to Equatable. The reason is, that it requires Self. Self refers to the concrete(e.g. struct/class) type that conforms to the Equatable. If you want to be able to use protocol instead of concrete type for array, then you need to write compression function yourself:

protocol Animal {
    
    var name: String { get }
    
}

func compare(lhsAnimals: [Animal], rhsAnimals: [Animal]) -> Bool {
    guard lhsAnimals.count == rhsAnimals.count else { return false}
    for i in 0..<lhsAnimals.count {
        if lhsAnimals[i].name != rhsAnimals[i].name {
            return false
        }
    }
    return true
}
Bulat Yakupov
  • 440
  • 4
  • 13
0

Thanks to @Alexander and his pointed video resource - https://youtu.be/_m6DxTEisR8?t=2585

Here's is the good workaround, to overcome the current limitation of Swift's protocol.

protocol Animal {
    func isEqual(to: Animal) -> Bool
}

func isEqual(lhs: [Animal], rhs: [Animal]) -> Bool {
    let count0 = lhs.count
    let count1 = rhs.count
    
    if count0 != count1 {
        return false
    }
    
    for i in 0..<count0 {
        if !(lhs[i].isEqual(to: rhs[i])) {
            return false
        }
    }
    
    return true
}

// struct. By conforming Equatable, struct is getting an auto 
// implementation of "static func == (lhs: Dog, rhs: Dog) -> Bool"
struct Dog: Animal, Equatable {
    func isEqual(to other: Animal) -> Bool {
        guard let other = other as? Dog else { return false }
        return self == other
    }
}

// class
class Cat: Animal, Equatable {
    static func == (lhs: Cat, rhs: Cat) -> Bool {
        // TODO:
        return true
    }
    
    func isEqual(to other: Animal) -> Bool {
        guard let other = other as? Cat else { return false }
        return self == other
    }
}

var animals0 = [Animal]()
var animals1 = [Animal]()

// Instead of using
// if animals0 == animals1 {
// we will use the following function call

if isEqual(lhs: animals0, rhs: animals1) {
    
}
Cheok Yan Cheng
  • 47,586
  • 132
  • 466
  • 875