Role of `Equatable` conformance when inserting element inside a `Set`

Hi all,

I am playing around with this sample code inside a Playground:

import Foundation

struct Operation: Hashable {
    let subject: String
    let type: OperationType
    init(subject: String, type: OperationType) {
        self.subject = subject
        self.type = type
    static func == (lhs: Operation, rhs: Operation) -> Bool {
        lhs.subject == rhs.subject
    enum OperationType: Hashable {
        case insert
        case update
        case delete

var pendingOperations: Set<Operation> = []

let insertSubject1 = Operation(subject: "1", type: .insert)

let updateSubject1 = Operation(subject: "1", type: .update)

insertSubject1 == updateSubject1



and I noticed that sometimes the second insertion succeed (returning (inserted: true, memberAfterInsert: Operation)) and sometimes not.

If I remove the explicit Equatable conformance, or I define it in a more appropriate way like:

    static func == (lhs: Operation, rhs: Operation) -> Bool {
        lhs.subject == rhs.subject && lhs.type == rhs.type

the second insertion always succeed as expected.

This led up to my questions:

  • Why is the behavior inconsistent when Equatable considers only a subset of the properties considered by Hashable ?

  • What is the role of Equatable and why is apparently not always honoured?

Your Equatable must match Hashable to not break the hash invariant ("equal contents → equal hash"). As you've overridden EQ you must also override hash:

    static func == (lhs: Operation, rhs: Operation) -> Bool {
        lhs.subject == rhs.subject
    func hash(into hasher: inout Hasher) {

Yes correct, but why, if I break the hash invariant, sometimes it behaves correctly and sometimes not?

I expected it to always behaves in the same way.

The Hasher instance has a different seed every time you run the program. What you’re observing could be that sometimes the hashes collide and sometimes they don’t, depending on the hash seed.


If I look at the values printed by insertSubject1.hashValue and updateSubject1.hashValue they are always different. Even when the second insertion fails.

Even with different hash values, the two items may end up in the same bucket of the set (= a hash collision). When that happens, Set will use == to compare the existing item(s) in that bucket with the one to be inserted. If it finds a match, it won't insert the new item.


Makes sense, thank you :slight_smile: