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 AnyHashable
s like this won't fare well in any performance-critical code.