I'm experimenting with using Composition instead of Inheritance and I wanted to use diff
on an array of objects that comply with a given protocol.
To do so, I implemented a protocol and made it comply with Equatable
:
// Playground - noun: a place where people can play
import XCPlayground
import Foundation
protocol Field:Equatable {
var content: String { get }
}
func ==<T: Field>(lhs: T, rhs: T) -> Bool {
return lhs.content == rhs.content
}
func ==<T: Field, U: Field>(lhs: T, rhs: U) -> Bool {
return lhs.content == rhs.content
}
struct First:Field {
let content:String
}
struct Second:Field {
let content:String
}
let items:[Field] = [First(content: "abc"), Second(content: "cxz")] //
The problem comes from a combination of the meaning of the
Equatable
protocol and Swift’s support for type overloaded functions.Let’s take a look at the
Equatable
protocol:What does this mean? Well it’s important to understand what “equatable” actually means in the context of Swift. “Equatable” is a trait of a structure or class that make it so that any instance of that structure or class can be compared for equality with any other instance of that structure or class. It says nothing about comparing it for equality with an instance of a different class or structure.
Think about it.
Int
andString
are both types that areEquatable
.13 == 13
and"meredith" == "meredith"
. But does13 == "meredith"
?The
Equatable
protocol only cares about when both things to be compared are of the same type. It says nothing about what happens when the two things are of different types. That’s why both arguments in the definition of==(::)
are of typeSelf
.Let’s look at what happened in your example.
You provided two overloads for the
==
operator. But only the first one has to do withEquatable
conformance. The second overload is the one that gets applied when you dowhich has nothing to do with the
Equatable
protocol.Here’s a point of confusion. Equability across instances of the same type is a lower requirement than equability across instances of different types when we’re talking about individually bound instances of types you want to test for equality. (Since we can assume both things being tested are of the same type.)
However, when we make an array of things that conform to
Equatable
, this is a higher requirement than making an array of things that can be tested for equality, since what you are saying is that every item in the array can be compared as if they were both of the same type. But since your structs are of different types, you can’t guarantee this, and so the code fails to compile.Here’s another way to think of it.
Protocols without associated type requirements, and protocols with associated type requirements are really two different animals. Protocols without
Self
basically look and behave like types. Protocols withSelf
are traits that types themselves conform to. In essence, they go “up a level”, like a type of type. (Related in concept to metatypes.)That’s why it makes no sense to write something like this:
You can write this:
or this:
or this:
Because
Int
,String
, andBool
are types.Equatable
isn’t a type, it’s a type of a type.It would make “sense” to write something like this…
though this is really stretching the bounds of type-safe programming and so Swift doesn’t allow this. You’d need a fully flexible metatyping system like Python’s to express an idea like that.
So how do we solve your problem? Well, first of all realize that the only reason it makes sense to apply SwiftLCS on your array is because, at some level, all of your array elements can be reduced to an array of keys that are all of the same
Equatable
type. In this case, it’sString
, since you can get an arraykeys:[String]
by doing[Field](...).map{ $0.content }
. Perhaps if we redesigned SwiftLCS, this would make a better interface for it.However, since we can only compare our array of
Field
s directly, we need to make sure they can all be upcast to the same type, and the way to do that is with inheritance.The array then upcasts them all to type
Field
which isEquatable
.By the way, ironically, the “protocol-oriented” solution to this problem actually still involves inheritance. The SwiftLCS API would provide a protocol like
We would specialize it with a superclass
and the library would use it as
Protocols and Inheritance are not opposites or substitutes for one another. They complement each other.
I know this is probably now what you want but the only way I know how to make it work is to introduce additional wrapper class:
and then you can do
I don't think you can make
Field
to conform toEquatable
at all as it is designed to be "type-safe" usingSelf
pseudo-class. And this is one reason for the wrapper class. Unfortunately there seems to be one more issue that I don't know how to fix: I can't put this "wrapped"diff
intoCollection
orArray
extension and still make it support heterogenous[Field]
array without compilation error:If anyone knows a better solution, I'm interested as well.
P.S.
In the question you mention that
but I expect that to be
true
given the way you defined your==
operator