Enum associated value : Hashable

Hi,

I have an enum with an associated value

I am using it in a dictionary (need it to be Hashable)

I am using the default implementation of Hashable which is great

enum Status : Hashable {
    
    case start
    case completed(String)
}

let s1 = Status.completed("A")
let s2 = Status.completed("B")

s1.hashValue == s2.hashValue //false

var statuses = [Status : String]()

statuses[s1] = "aaa"
statuses[s2] = "bbb"

print(statuses) //Ideally I would like to have just 1 value

Aim
For my specific scenario I would like to the hashValue to ignore the associated value.

Note: I suppose I could implement my own Hashable, but I don't know how to do it well (other than XOR)

Question:

  1. What would be a good approach to handle this ?
  2. I have mentioned a workaround (is that a good approach) or is there a better approach ?

Workaround (not sure if it is a good approach):

Implement rawValue a computed variable and let hashValue use the rawValue

enum Status : Hashable {
    
    case start
    case completed(String)

   //MARK: Hashable

    var hashValue: Int {
        
        return rawValue.hashValue
    }

   //MARK: Equatable
  
   //Need to this to make it equatable based on my logic (to ignore associated value)
    static func == (lhs: Mode, rhs: Mode) -> Bool {
        
        return lhs.rawValue == rhs.rawValue
    }
    
    private var rawValue : String {
        
        let value : String
        switch self {
        case .start:
            value = "start"
        case .completed(_):
            value = "completed"
        }
        
        return value
    }
}

let s1 = Status.completed("A")
let s2 = Status.completed("B")

s1.hashValue == s2.hashValue //true

var statuses = [Status : String]()

statuses[s1] = "aaa"
statuses[s2] = "bbb"

print(statuses) //1 value

Thanks.

2 Likes

Well, I'm certainly no expert on hashing, but your case seems like it could simply be solved by implementing your own hashValue

var hashValue: Int {
  switch self {
  case .start: return 0
  case .completed: return 1
  }
}

You also need to explicitly implement ==

static func ==(lhs: Status, rhs: Status) -> Bool {
  return lhs.hashValue == rhs.hashValue
}
2 Likes

Thanks @Letan

Oops my bad, I didn't read your post carefully.

I think that is a good idea to use Int instead of String, thanks a lot !!

Don't implement == by comparing hashValue though. That's just setting yourself up for hard-to-find bugs in the future.

1 Like

But I don't see the problem with this specific case though? Am I missing something or will this not always be the same. Because the hashValue's will never overlap. And any Equatable implementation that doesn't treat instances as equal if their hashValue is not equal will not do what the OP wanted.

It works right now, but if the implementation of hashValue changes so that collisions are possible, maybe due to a new case or so, it will fail in unpredictable, hard-to-diagnose ways. Relying on hashValue providing stronger guarantees than Hashable requires is not good practice, even if it works right nowโ„ข.

3 Likes

Thanks a lot @ahti and @Letan

Really interesting thoughts, I think I would have a private computed property rawValue just for clarity and use an Int instead of String rawValue

  • Use the rawValue for comparison of equality
  • Use rawValue.hashValue for getting the hashValue

You donโ€™t need an underlying property to implement equality. In ==, switch on the pair of values and ignore the associated values in the cases.

1 Like

Thanks @bzamayo, I agree with you, there is no need to have a property.

I was just thinking that I needed a switch statement for hashValue implementation and the same switch for ==, so was thinking to have one computed property to use in both implementations.