Skip to content

Commit ca58dee

Browse files
sjakobiclaude
andcommitted
Document that HashMaps have a canonical form
Closes #450. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
1 parent ef480de commit ca58dee

1 file changed

Lines changed: 26 additions & 0 deletions

File tree

Data/HashMap/Internal.hs

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -254,6 +254,25 @@ data HashMap k v
254254
-- * No two keys stored in a 'Collision' can be equal according to their
255255
-- 'Eq' instance. (INV10)
256256

257+
{-
258+
Note [Canonical form]
259+
~~~~~~~~~~~~~~~~~~~~~~~
260+
261+
The invariants above imply that HashMaps have a canonical form: two
262+
HashMaps that contain the same key-value pairs have the same tree
263+
structure, modulo the order of keys within a Collision node -- regardless
264+
of the order in which they were constructed. This is because each key's
265+
hash fully determines the path to its leaf, while INV3, INV5 and INV8
266+
rule out alternative encodings of the same sub-tree (e.g. a redundant
267+
BitmapIndexed node wrapping a single child, or a BitmapIndexed node that
268+
could be a Full node).
269+
270+
Several functions rely on this. They walk the leaves of two trees in tree
271+
order and compare or combine them pairwise, treating only Collision nodes
272+
as unordered -- for example, equal1. Without a canonical form these could
273+
give inconsistent results for maps with equal contents.
274+
-}
275+
257276
type role HashMap nominal representational
258277

259278
-- | @since 0.2.17.0
@@ -428,6 +447,7 @@ instance Eq k => Eq1 (HashMap k) where
428447
instance (Eq k, Eq v) => Eq (HashMap k v) where
429448
(==) = equal1 (==)
430449

450+
-- See Note [Canonical form]
431451
equal1 :: Eq k
432452
=> (v -> v' -> Bool)
433453
-> HashMap k v -> HashMap k v' -> Bool
@@ -444,6 +464,7 @@ equal1 eq = go
444464

445465
leafEq (L k1 v1) (L k2 v2) = k1 == k2 && eq v1 v2
446466

467+
-- See Note [Canonical form]
447468
equal2 :: (k -> k' -> Bool) -> (v -> v' -> Bool)
448469
-> HashMap k v -> HashMap k' v' -> Bool
449470
equal2 eqk eqv t1 t2 = go (leavesAndCollisions t1 []) (leavesAndCollisions t2 [])
@@ -478,6 +499,7 @@ instance Ord k => Ord1 (HashMap k) where
478499
instance (Ord k, Ord v) => Ord (HashMap k v) where
479500
compare = cmp compare compare
480501

502+
-- See Note [Canonical form]
481503
cmp :: (k -> k' -> Ordering) -> (v -> v' -> Ordering)
482504
-> HashMap k v -> HashMap k' v' -> Ordering
483505
cmp cmpk cmpv t1 t2 = go (leavesAndCollisions t1 []) (leavesAndCollisions t2 [])
@@ -504,6 +526,7 @@ cmp cmpk cmpv t1 t2 = go (leavesAndCollisions t1 []) (leavesAndCollisions t2 [])
504526
equalKeys1 :: (k -> k' -> Bool) -> HashMap k v -> HashMap k' v' -> Bool
505527
equalKeys1 eq t1 t2 = go (leavesAndCollisions t1 []) (leavesAndCollisions t2 [])
506528
where
529+
-- See Note [Canonical form]
507530
go (Leaf k1 l1 : tl1) (Leaf k2 l2 : tl2)
508531
| k1 == k2 && leafEq l1 l2
509532
= go tl1 tl2
@@ -520,6 +543,7 @@ equalKeys1 eq t1 t2 = go (leavesAndCollisions t1 []) (leavesAndCollisions t2 [])
520543
equalKeys :: Eq k => HashMap k v -> HashMap k v' -> Bool
521544
equalKeys = go
522545
where
546+
-- See Note [Canonical form]
523547
go :: Eq k => HashMap k v -> HashMap k v' -> Bool
524548
go Empty Empty = True
525549
go (BitmapIndexed bm1 ary1) (BitmapIndexed bm2 ary2)
@@ -532,6 +556,7 @@ equalKeys = go
532556

533557
leafEq (L k1 _) (L k2 _) = k1 == k2
534558

559+
-- See Note [Canonical form]
535560
instance Hashable2 HashMap where
536561
liftHashWithSalt2 hk hv salt hm = go salt (leavesAndCollisions hm [])
537562
where
@@ -558,6 +583,7 @@ instance Hashable2 HashMap where
558583
instance (Hashable k) => Hashable1 (HashMap k) where
559584
liftHashWithSalt = H.liftHashWithSalt2 H.hashWithSalt
560585

586+
-- See Note [Canonical form]
561587
instance (Hashable k, Hashable v) => Hashable (HashMap k v) where
562588
hashWithSalt salt hm = go salt hm
563589
where

0 commit comments

Comments
 (0)