Unclear Equatable performance

This is subtle and I'm not sure how Swift would handle it, since both ways are technically correct.

I have a protocol that implements equality between all instances of the protocol:

public protocol Drawable: Equatable {
    var width: Int { get }
    var height: Int { get }
    subscript(x: Int, y: Int) -> Color { get }
}

public extension Drawable {
    static func == (lhs: Self, rhs: Self) -> Bool {
        guard lhs.width == rhs.width && lhs.height == rhs.height else { return false }
        for x in 0..<lhs.width {
            for y in 0..<lhs.width {
                guard lhs[x, y] == rhs[x, y] else { return false }
            }
        }
        return true
    }
    
    static func == (lhs: Self, rhs: some Drawable) -> Bool {
        guard lhs.width == rhs.width && lhs.height == rhs.height else { return false }
        for x in 0..<lhs.width {
            for y in 0..<lhs.width {
                guard lhs[x, y] == .init(rhs[x, y]) else { return false }
            }
        }
        return true
    }
}

This is not efficient, but it is a correct way of comparing arbitrary drawables.

When types are known however, comparing individual pixels can be avoided, like for two circles:

public struct Circle: OptimizedDrawable, Hashable {
    public var width: Int { radius * 2 + 1 }
    public var height: Int { radius * 2 + 1 }
    public let color: Color
    public let radius: Int
    public let fill: Bool

    ...
}

I'm not sure though since I can't go-to-definition on operators in Xcode: is dedicated equality for two Cricles still derived because it inherits the Equatable requirement from Drawable? Does it just use the inefficient generic equality? If it isn't derived, does adding Equatable or Hashable explicitly derive it despite the existing generic implementation, or do I have to implement == for each drawable manually?

Also, when Swift derives Equatable or Hashable, does it ignore computed properties?

If your Circle type defines its own (more efficient) == function, the compiler will favor that more specific one over the less specific one with some Drawable, right?

Yeah I know, what I want to know is, do I have to define that more efficient function myself or will the compiler derive it for me. Or rather, does the generic == prevent the compiler from deriving Equatable as it definitely would otherwise.

Yes, your concrete types that refine Drawable will get Drawable's equality implementation unless they explicitly provide their own. This is a good example of a case where having better manual control of derived conformances would be handy.


I might consider making Drawable refine Equatable, but not provide a == implementation on Drawable, so that your concrete types get the efficient derived conformance, and then doing something like:

extension Drawable {
  func samePixels(as other: some Drawable) -> Bool {
    // pixel-by-pixel comparison
  }
}

so that you can use that implementation for your concrete types that have to fall back on pixelwise equality:

extension WeirdType: Drawable {
  static func ==(a: WeirdType, b: WeirdType) -> Bool {
    a.samePixels(as: b)
  }
}

This isn't a great solution; in particular, you have to know that the derived == won't work for WeirdType, there's no compile-time error to tell you that it's missing. But it probably reduces the amount of boilerplate that you have to write.

3 Likes