Why doesn't doesn't combine implicitly open Hashable?

I'm curious why hasher.combine can't implicitly open the any Hashable in this code?

struct Key: Hashable {

	let id: any Hashable

	init<ID: Hashable>(id: ID) {
		self.id = id
	}

	func hash(into hasher: inout Hasher) {
		hasher.combine(id) // shouldn't any Hashable get unboxed here?
//		id.hash(into: &hasher)  // this works as it should
	}

	static func == (lhs: Key, rhs: Key) -> Bool {
		lhs.id.isEqual(to: rhs.id)
	}

}

You can make this non generic:

    init(id: any Hashable) {
        self.id = id
    }

This:

hasher.combine(id)

doesn't work because the protocol (in this case Hashable) doesn't conform to itself.
For the same reason this doesn't compile either:

lhs.id.isEqual(to: rhs.id)

FWIW, the end result of these statements is the same:

hasher.combine(id) // if it worked
id.hash(into: &hasher)

According to SE-352, combine should unbox the Hashable. In fact, I can get it to work by pulling a piece of code from SE-352:

func testOpenSimple(hasher: inout Hasher, id: any Hashable) {
	hasher.combine(id) // this unboxes any Hashable correctly
}


struct Key: Hashable {

	let id: any Hashable

	init<ID: Hashable>(id: ID) {
		self.id = id
	}

	func hash(into hasher: inout Hasher) {
		testOpenSimple(hasher: &hasher, id: id)
	}

	static func == (lhs: Key, rhs: Key) -> Bool {
		lhs.id.isEqual(to: rhs.id)
	}
}

extension Equatable {
	func isEqual<E: Equatable>(to rhs: E) -> Bool {
		if let rhs = rhs as? Self {
			self == rhs
		} else {
			false
		}
	}
}

Key(id: a).hashValue

(note that the init is generic simply because it was pulled from other code :slight_smile: )

Also, I included isEqual so you can see why that works.

I see. BTW, do you except to see any difference in the result between this:

	func hash(into hasher: inout Hasher) {
		testOpenSimple(hasher: &hasher, id: id)
	}

and this:

	func hash(into hasher: inout Hasher) {
            id.hash(into: &hasher)
	}

Thank you. Beware of the gotcha of your implementation:

class C: Equatable {
    static func == (lhs: C, rhs: C) -> Bool { true }
}
class D: C {}

C().isEqual(to: D()) // true
D().isEqual(to: C()) // false

No, they both produce the same hash (as I would expect) though internally, one will call through the existential and the other will use the value directly.

And thanks for the reminder about the isEqual. I probably should have an assert in case I ever use it with a class hierarchy.