I'm working on a custom identifier protocol where I can provide an identifier from within the type. This works fine for structs but I ran into a wall when using it with an enum. Below is a quick example of what I'm hoping to do. This does not work since cases .state and .town pass self to hasher.combine(self). The compiler is happy since ExampleType conforms to Hashable, but results in infinite loop and crashes, since you can't actually use self.
Is there a way to avoid this infinite loop while using an enum? Is there a way to bypass my implementation and fallback to use the Hashable "default implementation" for certain enum cases?
public protocol CustomID: Hashable {
associatedtype ID: Hashable
var identifier: ID { get }
}
public enum ExampleType: Hashable, CustomID {
case city(name: String, address: Address)
case town
case state
public var identifier: AnyHashable {
switch self {
case let .city(name, _):
return name
case .state:
return self
case .town:
return self
}
}
}
extension ExampleType {
public func hash(into hasher: inout Hasher) {
hasher.combine(identifier)
}
public static func == (lhs: Self, rhs: Self) -> Bool {
return lhs.identifier == rhs.identifier
}
}
Are you sure you need a custom implementation here? This works out of the box:
struct Address: Hashable {
let someFieldHere: String
}
enum ExampleType: Identifiable, Hashable {
case city(name: String, address: Address)
case town
case state
var id: Self { self }
}
Ignoring part of properties in Hashable implementation IMO is an anti-pattern. I strongly recimmend against it. Equality of a == b means that it is safe to replace any usage of a with b and vice versa. But in you example it is not true for usages of ExampleType that access address.
If you are not using address - delete it altogether.
If you want to keep a collection of items unique by identifier - use Dictionary instead of Set.
If you want to express another relationship - give it an appropriate name, don’t call it Equatable/Hashable.
So removing extension for ExampleType would solve the infinite recursion.
In more general case, using name as identifier could be not unique enough - if there are other cases which also have string identifier. Or if you are mixing different types conforming to CustomID.
This can be solved using a parallel enum as an identifier:
public enum ExampleType: Hashable, CustomID {
case city(name: String, address: Address)
case town
case state
public enum ID: Hashable {
case city(name: String)
case town
case state
}
public var identifier: ID {
switch self {
case let .city(name, _):
return .city(name)
case .state:
return .state
case .town:
return .town
}
}
}
static func == (a: Self, b: Self) -> Bool {
a.id == b.id
}
func hash(into hasher: inout Hasher) {
hasher.combine(id)
}
var id: AnyHashable {
switch self {
case .city(let name, _):
return "city: " + name
case .town:
return "town"
case .state:
return "state"
}
}
Note that identical values might be necessarily equal and equal values might be necessarily identical, but strictly speaking these are two different features (e.g. two reference objects might be equal but not identical, and two identical objects might have EQ implementation that always returns false). In some way "equivalence" is more connected to "hashability" (and vice versa) than to "identity".