`protocol Drawable {}` vs `protocol Drawable : Equatable {}`

In the Crustacean example, what is the apriori rationale (the rationale before the compiler detects an error and one is forced to find an alternate rationale) to avoiding


protocol Drawable : Equatable { /* ... compiler error : self constraints ... */ }


in favor of


protocol Drawable { /* ... */ }


Is the rationale:

  1. define protocols, such as `Drawable`, with no constraints, such as `Equatable`, whenever possible. Add constraints with `extension`. Avoid `Self` constaints like the plague.
  2. we know `Drawable` will be equatable and we also know some drawables, such as `Diagram`, will be composed of drawables with various subtypes - we thus must avoid any constraint, such as `Equatable`, that imposes a `Self` constraint.


One of these? Some other rationale?

Anybody? How do I know not to use `protocol Drawable : Equatable {}`?

Accepted Answer

I am not sure if I get the your question.


There are basicly two categories of protocols: (1) Those without any Self references or Type aliases and (2) those with Self references and Type aliases.


If you want your protocol to act as super type for various type which can be mixed at runtime (such as multiple drawables) your protocol needs to stay in category (1). In category (1) method calls are dynamicly dispatched. The type of an variable does not need to be narrowed down at compile time.


Protocol of category (2) can only be used as constraint on generic types. A collection(eg array) can not contain elements of various types with just this protocol in comon. Method calls are statically dispatched at compile time. Therefor the compiler does need to already know the variable's final type. You can not use such a protocol in a variable declaration.


Protocol like Equatable and Hashable belong to category (2). Therefor all protocols extending Equatable or Hashable belong to category (2).


=> If you want your protocol to belong to category (1) it must not extend Equatable or Hashable. In the Drawing example you want the Drawable protocol to be in category (1) because you want to have a collection of drawables with mixed types whose concrete type can not be known at compile time.


You want to be able to compare drawables. But you want any drawable to be compareable with any other Drawable. Eg you want to be able to compare a Circle to a Rectangle. But that's not what Equatable is made for. The Equatable protocol is made for comparing two entities with the same type (Rectangle vs Rectangle, Circle vs Circle). So from this perspective the Equatable protocol was not want you wanted in the first place.


Here is a reduced example without using Equatable at all. Neither in the Drawable protocol nor in the Circle or Diagram implementation:


protocol Drawable {
    func isSameAs(other: Drawable) -> Bool
}

struct Circle : Drawable {
  let x: Double
  let y: Double
  let r: Double
  func isSameAs(other: Drawable) -> Bool {
    guard let other = other as? Circle else { return false }
  
    return x == other.x && y == other.y && r == other.r // compare circles by their components
  }
}

class Diagram : Drawable {
  func isSameAs(other: Drawable) -> Bool {
    guard let other = other as? Diagram else { return false }
  
    return self === other // just check if the objects are the same
  }
}


In the end you do not need the Equatable protocol to compare two entities. You might want to use the Equatable protocol to make to entities of the same type comparable.

IMO, the answer is, "You don't", and this is a huge defect in the current Swift.


The problem is that using a Self constraint (like Equatable does) effectively makes the protocol generic, or something analogous to generic. Thus, the problem with what you want to do is that it "means":


protocol Drawable : Equatable<T> { … }


which clearly can't be acceptable. What you wanted, in these terms, would be :


protocol Drawable<T> : Equatable<T> { … }


but there's no way of doing that.


Unfortunately, the reasoning is inscrutable until you're fairly deep into Swift protocols. I want to believe there will be a major language shakeup that resolves this somehow (perhaps by getting rid of typealias constraints and implementing real generic protocols, though of course I have no idea what the technical feasibility of that might be). Otherwise, the beginning of Laszlo's answer ("there are 2 kinds of protocols" and a shrug) is about as far as most people will get.

So, in defining the `Drawable` protocol from the get go, we anticipate that some subtype will want heterogeneous collections of `Drawables` and thus we must not impose a `Self` constraint with either `Equatable` or `Hashable` or others. Given that it is a bit difficult to forecast how a protocol will be subsequently used, one's default needs to be 'avoid Self constraints' and, in concert, 'let the subtypes work it out for themselves with 'extension Equatable where Self:...'' or, as you show, custom `isSameAs()` or similar.


Eventually some subtype will need to be Equatable, Hashable, etc (so they can be stored in an Array or Set, for example) and only then do we impose the Self constraint, on some 'base class'. As such:

protocol Person {
  var fullname : String { get }
  func hasFriend(p:Person) -> Bool
}

class PersonBase : Person, Hashable {
  var fullname = "A. Random Person" 
  var friends  = Set<PersonBase>()

  func hasFriend(p: Person) -> Bool {
    guard let f = p as? PersonBase else { return false }
    return friends.contains(f)
  }

  var hashValue : Int {
    return fullname.hashValue
  }
}

func == (lhs:PersonBase, rhs:PersonBase) -> Bool {
  return lhs.fullname == rhs.fullname
}

> 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

&#96;protocol Drawable {}&#96; vs &#96;protocol Drawable : Equatable {}&#96;
 
 
Q