> Given that it is a bit difficult to forecast how a protocol will be subsequently used
That's what I thought at first, too. After some hours of discussion I realized that that seems to be my (and maybe your's and others') misconception about protocols:
In reality you know excactly how a protocol will be used because you design it for a special usecase ie. the protocol defines the usecase.
I will try to discuss this in the following:
In the case of Equatable the usecase is "compare two entities of the same type"
Hashable builds ontop of this usecase. "get a hashvalue for an entity which is comparable with entites of it's own type"
The Dictionary struct - by relying on the Hashable protocol - explicitly defines it's usecase as "Mapping homogenous keys to (heterogenous) values"
In the same way the Set struct explicitly states to only containing homogenous data - by relying on the Hashable protocol, transitively relying on the Equatable protocol.
The usecase for Equatable is not "compare suff" but "compare homogenous stuff". This differs from eg `boolean equals(Object o)` in Java, `- (BOOL)isEqual:(id)object` in objc or semilar methods in other OOP languages.
If your usecase is to compare heterogenous data you could create a protocol for this (even using the same == operator):
protocol EquatableHetero {
func equalsTo(rhs: EquatableHetero) -> Bool
}
struct MyStruct : EquatableHetero {
func equalsTo(rhs: EquatableHetero) -> Bool {
return true
}
}
func ==(lhs: EquatableHetero, rhs: EquatableHetero) -> Bool {
return lhs.equalsTo(rhs)
}
But you will notice that there is certainly no common usecase for this. You never want to compare two things of ANY two types with each other (the equality methods in Java and objc are kind of flawed). Simply because it makes no sense. Having a method/function to compare an email address with an NSWindows just makes no sense even if it would just return false. It is like having a method y.isParentOf(x) which can be called on two integers always returning false because obviously there is no parent/child relationship between two numbers. So I assume that is the reason there is no heterognous comparsion protocol in the standard library: There is no case in which you want to use it.
Now in you Drawable example you have a new usecase which is pretty good defined by the domain: You want to have multiple kinds of drawable stuff which can be mixed with each other. You want to be compare one drawable thing with another drawable thing but you do not require to be the two things to be of the same kind. In such a usecase the question "does this circle is the same as this rectangle" makes sense from the point of view that both are drawables. But you would still not want to ask a question like "does this circle the the same as this email address" so you still do not want total heterogenous comparison but juse heterogenous comparison INSIDE you domain.
This makes the usecase pretty well defined and leads naturally to your Drawable protocol containing a method for comparing other Drawables.
(Having some background in java) for some time I felt the urge to implement Hashable for many of my types just because I enjoy putting stuff into HashMaps (O(1)) and from a java point of view a hashvalue is just some natural property of all objects. I am working on a drawing application myself.
Not being able to make my Drawable protocol Hashable without losing the heterogeneity of the Drawables anoyed me for a long time. But now looking at it from the usecase perspective I see that I do not want to the Drawable to be Hashable in the first place, because the Hashable protocol already defines a very specific use case of homegenous data which is not the usecase I have. I want to put my drawables into a "Set" but not into a Swift.Set because Swift.Set is made for homegenous data - I want mixed data. This leads to two options: (1) implement my own DrawableSet which uses my equalTo() method (and an additional hashValue:Int I add to my protocol) or (2) writing an adapter which wraps/encapsulates the heterogeneity of my Drawabls and exposes a homogenous Type
Option (2) is what the standard library itself does with it's Any* types. How this is done is already explained in some other threads in blog posts. It some manual implemented Type erasure. Here a quick example:
protocol Drawable {
func equalsTo(rhs: Drawable) -> Bool
var hashValue : Int { get }
}
struct Point : Drawable {
var x: Double
var y: Double
func equalsTo(rhs: Drawable) -> Bool {
guard let other = rhs as? Point else {
return false
}
return x==other.x && y==other.y
}
var hashValue : Int {
return 13 * Int(x) + Int(y)
}
}
struct AnyDrawable : Hashable {
let real : Drawable
var hashValue : Int { return real.hashValue }
}
func ==(lhs: AnyDrawable, rhs: AnyDrawable) -> Bool {
return lhs.real.equalsTo(rhs.real)
}
var set = Set<AnyDrawable>()
set.insert(AnyDrawable(real: Point(x:1,y:3)))
if let drawable = set.first?.real {
//....
}
A Drawable does neither implement the Equatable nor the Hashable protocol nevertheless you can compare drawables with eachother and ask a drawable for a hashValue - because the Drawable protocol defines the methods needed to to so. The AnyDrawable acts as an adapter between drawables and the Set