A currency key type treats 100 cents and 1.00 dollars as equal after normalization — what does that require of its hash function?
answer
- equality here is semantic, not field-by-field
- what form do the two representations share?
- the hash must read what equality reads
- canonicalize first, then hash the canonical form
basics
~20 sThe hash must be computed from the same normalized form the equality check compares — for example total cents — so every representation that compares equal hashes identically. Hashing the raw amount-and-unit fields lets equal keys hash apart, and lookups silently miss.
solid answer
~40 sEquality here is semantic: normalize both amounts to a canonical form (say, total cents), then compare. The contract forces the hash to follow the same semantics — hash the canonical form, never the raw fields. Otherwise an invoice stored under a key built as `1.00 dollar` and looked up as `100 cents` compares equal to its stored twin but hashes into a different bucket: the paid-invoice lookup returns nothing and the invoice is flagged unpaid, with no error anywhere. The general principle: whatever fields the equality check reads, in whatever normalized form, the hash must read exactly those. Reading a strict subset is legal — equal objects still hash equal, at the price of extra collisions — but reading anything equality ignores, including un-normalized raw fields, breaks the contract.
go deeper
Be ready to state that the hash and the equality check must agree on which representations count as the same key, and that hashing raw fields under normalizing equality loses entries silently.
Explain the subset rule: hash inputs must be a function of what equality compares — a subset is legal at the cost of collisions, anything equality ignores is a contract violation. Walk the currency trace end to end.
Show the detection instincts: spot equality that transforms its operands next to a hash that does not, and reach for a property test asserting equal implies equal hash across representation variants.
Own the design answer: canonicalize at the system boundary so key objects never carry dual representations, making the contract hold by construction instead of by discipline.
## When equality is semantic, hashing must be too Many key types define equality field-by-field, and for those, hashing the same fields is straightforward. The trap appears when equality is *semantic*: two different in-memory representations are declared equal because they mean the same thing. A currency amount is the classic case — `100 cents` and `1.00 dollars` are the same money, so the equality check normalizes both to a canonical unit before comparing. Case-insensitive identifiers, file paths with and without a trailing separator, numerically equal values stored at different precisions, and timestamps carried in different zones all have the same shape. The equality-hash contract says equal objects must produce equal hash values. With semantic equality, the *only* way to guarantee that is to compute the hash from the **canonical form** — the normalized value equality actually compares — not from the raw stored fields. Two representations of the same amount share their canonical form by definition; they do not share their raw fields. ## Tracing the failure A billing system keys a map of paid invoices by a currency-amount type. The insert path builds the key from a settlement feed as `(1, "dollar")`; the reconciliation path rebuilds it from a ledger as `(100, "cent")`. Equality normalizes both to 100 total cents and says *equal*. But the hash was written over the raw pair `(amount, unit)`, so the two keys hash differently, land in different buckets, and the reconciliation lookup finds nothing. The invoice is marked unpaid, a reminder goes out, and no exception was ever thrown — the map did exactly what it was told, with inconsistent instructions. ## The subset rule A precise way to hold the whole constraint in your head: - **Hash inputs must be a function of what equality compares.** Reading exactly the canonical fields is the norm. - **A strict subset is legal.** If equality compares normalized amount *and* currency code, a hash over the amount alone still maps equal keys together — it just also maps some unequal keys together, which is an ordinary collision handled by the equality check. Cost: longer bucket chains, never wrong answers. - **A superset — or a different form — is illegal.** Any input that equality ignores (an un-normalized field, a display string, a creation timestamp) can differ between two equal keys, letting them hash apart. That is the contract violation, and it fails silently as missed lookups and phantom duplicates. This is also why the practical advice is to **normalize once, at the boundary**, and store the canonical form: if the object only ever holds 100-cents form, equality and hashing over its fields agree by construction, and the whole class of bug disappears. ## How to catch it The bug hides from example-based tests, which tend to build both sides of an assertion the same way. The reliable net is a small property test over the key type itself: generate pairs of representations that must compare equal — `(1, dollar)` and `(100, cent)`, mixed-case spellings, path variants — and assert both `a equals b` and `hash(a) == hash(b)`. The second assertion is the one teams forget, and it is precisely the contract. In review, the smell to scan for is any equality check that transforms its operands — lowercasing, trimming, unit conversion, rounding — while the hash function nearby reads bare fields. The two must transform identically or share one canonicalization routine.
- Is it legal for the hash to read fewer fields than the equality check compares?Yes. A subset keeps the contract: keys that compare equal agree on every compared field, so they agree on any subset, and hash equal. The cost is extra collisions — unequal keys differing only in the omitted fields share buckets — which slows lookups but never corrupts them. Only inputs equality ignores are illegal.
- Where else does this normalized-equality trap show up besides currency?Anywhere equality transforms before comparing: case-insensitive identifiers hashed over raw case-sensitive text, paths compared with trailing separators stripped but hashed verbatim, numeric values equal across widths or precisions but hashed from their bit patterns, timestamps equal as instants but hashed with their zone attached. The fix is always the same — hash the canonical form.
- How would you test a key type against this bug?Property-test the contract directly: generate pairs of representations required to compare equal — unit variants, case variants, precision variants — and assert both that they compare equal and that their hashes match. Example-based tests usually construct both operands identically, which is exactly why this bug survives them.
A filing clerk who agrees that '1.00 dollar' and '100 cents' name the same invoice, but files by the literal text on the slip, puts them in different drawers — whether you find the invoice depends on which spelling you ask with.
saying these in an interview costs you the question
- Hashing every raw field of the object is always the safe choice
- Equality can normalize while the hash reads raw fields, as long as both are defined
- A wrong bucket is recovered because the table eventually compares all keys
- A hash reading fewer fields than equality compares violates the contract