Why does Hashable require Equatable?

Let's return to the concrete problem of keeping Equatable and Hashable conformances in sync. Consider the code below, adapted from an earlier example:

struct Bar { 
  let a, b: Int 
  let c: String
}

extension Bar: Equatable {
  static func ==(left: Bar, right: Bar) -> Bool {
    // Silly and bogus example illustrating a complex case -- do not copy
    guard left.a * left.b == right.a * right.b else { return false }
    guard left.a + left.b == right.a + right.b else { return false }
    guard left.c == right.c else { return false }
    return true
  }
}

extension Bar: Hashable {
  func hash(into hasher: inout Hasher) {
    // Silly and bogus example illustrating a complex case -- do not copy
    hasher.combine(a * b)
    hasher.combine(a + b)
    hasher.combine(c)
  }
}

There are obvious similarities between the == and hash(into:) definitions above. You can almost define == in terms of the underlying hash encoding that a properly written hash(into:) feeds to the hasher.

hash(into:) is also eerily, annoyingly similar to encode(to:) from Encodable. You could in fact define both Equatable and Hashable in terms of encode(to:), by encoding Bar to an explicit byte sequence using a stable encoder, then comparing/hashing these encodings. But that would be terribly inefficient.

Here is a draft of a slightly better way:

protocol Distillable: Hashable {
  associatedtype Essence: Hashable
  var essence: Essence { get }
}

extension Distillable {
  static func ==(left: Self, right: Self) -> Bool {
    return left.essence == right.essence
  }

  func hash(into hasher: inout Hasher) {
    hasher.combine(essence)
  }
}

struct Bar: Distillable { 
  let a, b: Int 
  let c: String
  var essence: [AnyHashable] { return [a * b, a + b, c] }
}

Note how you only need to define the relevant components of Bar once, in essence.

Note that I haven't actually tried this code -- it's merely intended to illustrate a concept. However, something like this may actually be practical in some cases. Just be aware that creating an array of AnyHashables like this won't fare well in any performance-critical code.

6 Likes