Heterogeneous Constraints with Protocols

The lack of availability of creating an heterogeneous container (e.g. Array<Equatable>) says that when it has Self, or a typealias, like many of Swift's own protocols, it can not be used as the example given.


Is this a compiler limitation, or simply how things are designed to be?

I believe it is by design. I recommend watching the excellent WWDC 2015 session 408 video on Protocol Oriented Programming which covers this topic in detail. Basically there are two worlds of protocols (those with and those without Self constraints). If necessary, you can bridge between them using constrained protocol extensions to take advantage of the Self constraints of one protocol (e.g. Equatable) in the context of another protocol without Self constraints.

Ok, I saw the presentation and I got a better idea of it, thanks. If you don't mind could you make some mock code as to understand correctly what you menat to say about the bridge between protocols?

I was just referring to the example in the video where he has a Drawable protocol which is a plain (non-Self constrained) protocol. That protocol may be implemented by many other classes which are Drawable and he wants to deal with a Diagram that may contain any number of heterogeneous Drawable items. So far that is all okay. Then he wants to test for equality between two Drawable items, but as you know Equatable doesn't work in this case since it has a Self constraint which means equality can only be tested between two items of the same type and you can't mix them within a heterogeous collection. So he proposes something along the lines of the following code for Drawable:


protocol Drawable {
     func isEqualTo(other: Drawable) -> Bool
     ...  /* other methods here */
}


This means any two Drawable items can be compared for equality, but one still has to implement the test. So, this is where bridging happens using an extension. He implements isEqualTo() in an extension which provides an implementation for Drawable items that are also Equatable. This code can look something like the following:

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

Just be aware that this kind of equality test recreates a flaw in objc equality testing, and is very fragile and problematic in one of the ways that Swift was intended to fix.


It only works dependably if all types which conform to the protocol are value-types, and can give false positives if the two Drawable instances involved have types that are parent and subclass.


For example (in a playground in Xcode 7 beta 3):

protocol Place
{
    var size: CGSize {get}

    func isEqualToPlace(place: Place) -> Bool
}

extension Place where Self: Equatable
{
    func isEqualToPlace(place: Place) -> Bool
    {
        guard let other = place as? Self else { return false }
        return self == other
    }
}


class EmptyLot: Equatable, Place
{
    var size: CGSize = CGSize(width: 10, height: 10)
}

func == (lhs: EmptyLot, rhs: EmptyLot) -> Bool {return lhs.size == rhs.size}


class Minefield: EmptyLot
{
    var hasMines: Bool = true
}

func == (lhs: Minefield, rhs: Minefield) -> Bool {return (lhs.size == rhs.size) && (lhs.hasMines == rhs.hasMines)}


let x = EmptyLot()
let y = Minefield()

x.isEqualToPlace(y)     // true
y.isEqualToPlace(x)     // false


There is nothing you can do to the subclass which would make this code work correctly, so if the base class and protocol are in a framework the only solution would be to not inherit the class and rewrite all of the functionality that was in the base class, assuming the developer was even aware that there was a problem.



If you want instances to only potentially be equal if they have the exact same type, you end up needing to do something like this.


extension Place
{
    func isActuallyEqualToPlace(place: Place) -> Bool
    {
        return self.mightBeEqualToPlace(place) && place.mightBeEqualToPlace(self)
    }

    func mightBeEqualToPlace(place: Place) -> Bool
    {
        guard let other = place as? Self else { return false }
        return self == other
    }
}


x.isActuallyEqualToPlace(y)     // true
y.isActuallyEqualToPlace(x)     // true

I can't fix my post because editing is currently broken, but those last two statements actually return false, not true...

Good point! Maybe another pitfall with using classes.


How about:

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

Yes, that definitely works better than splitting it into two methods. I had forgotten that dynamicType started supporting == in Swift 2, and I couldn't come up with any other tests that would distinguish between parent and subclass without calling a second protocol-declared method on both instances.

Heterogeneous Constraints with Protocols
 
 
Q