hash-cons 0.1.0.0 → 0.2.0.0
raw patch · 10 files changed
+643/−91 lines, 10 filesPVP ok
version bump matches the API change (PVP)
API changes (from Hackage documentation)
- Data.HashCons.Internal: HashCons :: {-# UNPACK #-} !Int -> {-# UNPACK #-} !IORef a -> HashCons a
+ Data.HashCons: pattern HashCons :: Hashable a => a -> HashCons a
+ Data.HashCons.IntMap: data IntMap a
+ Data.HashCons.IntMap: difference :: Hashable a => IntMap a -> IntMap a -> IntMap a
+ Data.HashCons.IntMap: differenceNonEmpty :: Hashable a => NonEmptyIntMap a -> IntMap a -> IntMap a
+ Data.HashCons.IntMap: empty :: IntMap a
+ Data.HashCons.IntMap: insert :: Hashable a => Int -> a -> IntMap a -> IntMap a
+ Data.HashCons.IntMap: insertNonEmpty :: Hashable a => Int -> a -> NonEmptyIntMap a -> NonEmptyIntMap a
+ Data.HashCons.IntMap: intersection :: Hashable a => IntMap a -> IntMap a -> IntMap a
+ Data.HashCons.IntMap: intersectionNonEmpty :: Hashable a => NonEmptyIntMap a -> IntMap a -> IntMap a
+ Data.HashCons.IntMap: lookup :: Int -> IntMap a -> Maybe a
+ Data.HashCons.IntMap: lookupNonEmpty :: Int -> NonEmptyIntMap a -> Maybe a
+ Data.HashCons.IntMap: map :: Hashable b => (a -> b) -> IntMap a -> IntMap b
+ Data.HashCons.IntMap: mapWithKey :: Hashable b => (Int -> a -> b) -> IntMap a -> IntMap b
+ Data.HashCons.IntMap: member :: Int -> IntMap a -> Bool
+ Data.HashCons.IntMap: singleton :: Hashable a => Int -> a -> IntMap a
+ Data.HashCons.IntMap: singletonNonEmpty :: Hashable a => Int -> a -> NonEmptyIntMap a
+ Data.HashCons.IntMap: traverseWithKey :: (Applicative t, Hashable b) => (Int -> a -> t b) -> IntMap a -> t (IntMap b)
+ Data.HashCons.IntMap: union :: Hashable a => IntMap a -> IntMap a -> IntMap a
+ Data.HashCons.IntMap: unionNonEmpty :: Hashable a => NonEmptyIntMap a -> IntMap a -> NonEmptyIntMap a
+ Data.HashCons.Internal: HashConsC :: {-# UNPACK #-} !Int -> {-# UNPACK #-} !IORef a -> HashCons a
+ Data.HashCons.Internal: compareAndSubstitute :: Eq r => (a -> a -> r) -> r -> IORef a -> IORef a -> r
+ Data.HashCons.Internal: pattern HashCons :: Hashable a => a -> HashCons a
+ Data.HashCons.WordMap: data NonEmptyWordMap a
+ Data.HashCons.WordMap: data WordMap a
+ Data.HashCons.WordMap: difference :: Hashable a => WordMap a -> WordMap a -> WordMap a
+ Data.HashCons.WordMap: differenceNonEmpty :: Hashable a => NonEmptyWordMap a -> WordMap a -> WordMap a
+ Data.HashCons.WordMap: empty :: WordMap a
+ Data.HashCons.WordMap: insert :: Hashable a => Word -> a -> WordMap a -> WordMap a
+ Data.HashCons.WordMap: insertNonEmpty :: Hashable a => Word -> a -> NonEmptyWordMap a -> NonEmptyWordMap a
+ Data.HashCons.WordMap: intersection :: Hashable a => WordMap a -> WordMap a -> WordMap a
+ Data.HashCons.WordMap: intersectionNonEmpty :: Hashable a => NonEmptyWordMap a -> WordMap a -> WordMap a
+ Data.HashCons.WordMap: lookup :: Word -> WordMap a -> Maybe a
+ Data.HashCons.WordMap: lookupNonEmpty :: Word -> NonEmptyWordMap a -> Maybe a
+ Data.HashCons.WordMap: map :: Hashable b => (a -> b) -> WordMap a -> WordMap b
+ Data.HashCons.WordMap: mapNonEmpty :: Hashable b => (a -> b) -> NonEmptyWordMap a -> NonEmptyWordMap b
+ Data.HashCons.WordMap: mapWithKey :: Hashable b => (Word -> a -> b) -> WordMap a -> WordMap b
+ Data.HashCons.WordMap: mapWithKeyNonEmpty :: Hashable b => (Word -> a -> b) -> NonEmptyWordMap a -> NonEmptyWordMap b
+ Data.HashCons.WordMap: member :: Word -> WordMap a -> Bool
+ Data.HashCons.WordMap: memberNonEmpty :: Word -> NonEmptyWordMap a -> Bool
+ Data.HashCons.WordMap: singleton :: Hashable a => Word -> a -> WordMap a
+ Data.HashCons.WordMap: singletonNonEmpty :: Hashable a => Word -> a -> NonEmptyWordMap a
+ Data.HashCons.WordMap: traverseWithKey :: (Applicative t, Hashable b) => (Word -> a -> t b) -> WordMap a -> t (WordMap b)
+ Data.HashCons.WordMap: traverseWithKeyNonEmpty :: (Applicative t, Hashable b) => (Word -> a -> t b) -> NonEmptyWordMap a -> t (NonEmptyWordMap b)
+ Data.HashCons.WordMap: union :: Hashable a => WordMap a -> WordMap a -> WordMap a
+ Data.HashCons.WordMap: unionNonEmpty :: Hashable a => NonEmptyWordMap a -> WordMap a -> NonEmptyWordMap a
+ Data.HashCons.WordMap.Internal: Bin :: !Word -> !Word -> WordMap a -> WordMap a -> NonEmptyWordMap a
+ Data.HashCons.WordMap.Internal: Empty :: WordMap a
+ Data.HashCons.WordMap.Internal: Leaf :: !Word -> a -> NonEmptyWordMap a
+ Data.HashCons.WordMap.Internal: NonEmptyMap :: !HashCons (NonEmptyWordMap a) -> WordMap a
+ Data.HashCons.WordMap.Internal: branchingBit :: Word -> Word -> Word
+ Data.HashCons.WordMap.Internal: combine :: Hashable a => Word -> Word -> WordMap a -> WordMap a -> WordMap a
+ Data.HashCons.WordMap.Internal: data NonEmptyWordMap a
+ Data.HashCons.WordMap.Internal: data WordMap a
+ Data.HashCons.WordMap.Internal: difference :: Hashable a => WordMap a -> WordMap a -> WordMap a
+ Data.HashCons.WordMap.Internal: differenceNonEmpty :: Hashable a => NonEmptyWordMap a -> WordMap a -> WordMap a
+ Data.HashCons.WordMap.Internal: empty :: WordMap a
+ Data.HashCons.WordMap.Internal: highestBitMask :: Word -> Word
+ Data.HashCons.WordMap.Internal: insert :: Hashable a => Word -> a -> WordMap a -> WordMap a
+ Data.HashCons.WordMap.Internal: insertNonEmpty :: Hashable a => Word -> a -> NonEmptyWordMap a -> NonEmptyWordMap a
+ Data.HashCons.WordMap.Internal: instance Data.Hashable.Class.Hashable a => Data.Hashable.Class.Hashable (Data.HashCons.WordMap.Internal.NonEmptyWordMap a)
+ Data.HashCons.WordMap.Internal: instance Data.Hashable.Class.Hashable a => Data.Hashable.Class.Hashable (Data.HashCons.WordMap.Internal.WordMap a)
+ Data.HashCons.WordMap.Internal: instance GHC.Classes.Eq a => GHC.Classes.Eq (Data.HashCons.WordMap.Internal.NonEmptyWordMap a)
+ Data.HashCons.WordMap.Internal: instance GHC.Classes.Eq a => GHC.Classes.Eq (Data.HashCons.WordMap.Internal.WordMap a)
+ Data.HashCons.WordMap.Internal: instance GHC.Classes.Ord a => GHC.Classes.Ord (Data.HashCons.WordMap.Internal.NonEmptyWordMap a)
+ Data.HashCons.WordMap.Internal: instance GHC.Classes.Ord a => GHC.Classes.Ord (Data.HashCons.WordMap.Internal.WordMap a)
+ Data.HashCons.WordMap.Internal: instance GHC.Generics.Generic (Data.HashCons.WordMap.Internal.NonEmptyWordMap a)
+ Data.HashCons.WordMap.Internal: instance GHC.Generics.Generic (Data.HashCons.WordMap.Internal.WordMap a)
+ Data.HashCons.WordMap.Internal: instance GHC.Show.Show a => GHC.Show.Show (Data.HashCons.WordMap.Internal.NonEmptyWordMap a)
+ Data.HashCons.WordMap.Internal: instance GHC.Show.Show a => GHC.Show.Show (Data.HashCons.WordMap.Internal.WordMap a)
+ Data.HashCons.WordMap.Internal: intersection :: Hashable a => WordMap a -> WordMap a -> WordMap a
+ Data.HashCons.WordMap.Internal: intersectionNonEmpty :: Hashable a => NonEmptyWordMap a -> WordMap a -> WordMap a
+ Data.HashCons.WordMap.Internal: join :: Hashable a => Word -> NonEmptyWordMap a -> Word -> NonEmptyWordMap a -> NonEmptyWordMap a
+ Data.HashCons.WordMap.Internal: lookup :: Word -> WordMap a -> Maybe a
+ Data.HashCons.WordMap.Internal: lookupNonEmpty :: Word -> NonEmptyWordMap a -> Maybe a
+ Data.HashCons.WordMap.Internal: map :: Hashable b => (a -> b) -> WordMap a -> WordMap b
+ Data.HashCons.WordMap.Internal: mapNonEmpty :: Hashable b => (a -> b) -> NonEmptyWordMap a -> NonEmptyWordMap b
+ Data.HashCons.WordMap.Internal: mapWithKey :: Hashable b => (Word -> a -> b) -> WordMap a -> WordMap b
+ Data.HashCons.WordMap.Internal: mapWithKeyNonEmpty :: Hashable b => (Word -> a -> b) -> NonEmptyWordMap a -> NonEmptyWordMap b
+ Data.HashCons.WordMap.Internal: mask :: Word -> Word -> Word
+ Data.HashCons.WordMap.Internal: match :: Word -> Word -> Word -> Bool
+ Data.HashCons.WordMap.Internal: member :: Word -> WordMap a -> Bool
+ Data.HashCons.WordMap.Internal: memberNonEmpty :: Word -> NonEmptyWordMap a -> Bool
+ Data.HashCons.WordMap.Internal: shorter :: Word -> Word -> Bool
+ Data.HashCons.WordMap.Internal: singleton :: Hashable a => Word -> a -> WordMap a
+ Data.HashCons.WordMap.Internal: singletonNonEmpty :: Hashable a => Word -> a -> NonEmptyWordMap a
+ Data.HashCons.WordMap.Internal: traverseWithKey :: (Applicative t, Hashable b) => (Word -> a -> t b) -> WordMap a -> t (WordMap b)
+ Data.HashCons.WordMap.Internal: traverseWithKeyNonEmpty :: (Applicative t, Hashable b) => (Word -> a -> t b) -> NonEmptyWordMap a -> t (NonEmptyWordMap b)
+ Data.HashCons.WordMap.Internal: union :: Hashable a => WordMap a -> WordMap a -> WordMap a
+ Data.HashCons.WordMap.Internal: unionNonEmpty :: Hashable a => NonEmptyWordMap a -> WordMap a -> NonEmptyWordMap a
+ Data.HashCons.WordMap.Internal: unionNonEmptyNonEmpty :: Hashable a => NonEmptyWordMap a -> NonEmptyWordMap a -> NonEmptyWordMap a
+ Data.HashCons.WordMap.Internal: zero :: Word -> Word -> Bool
Files
- hash-cons.cabal +11/−3
- src/Data/HashCons.hs +2/−0
- src/Data/HashCons/IntMap.hs +105/−0
- src/Data/HashCons/IntMap/Internal.hs +1/−0
- src/Data/HashCons/Internal.hs +70/−23
- src/Data/HashCons/WordMap.hs +29/−0
- src/Data/HashCons/WordMap/Internal.hs +286/−0
- test/Data/HashCons/IntMap/Test.hs +63/−0
- test/Data/HashCons/Internal/Test.hs +71/−0
- test/Main.hs +5/−65
hash-cons.cabal view
@@ -1,5 +1,5 @@ name: hash-cons-version: 0.1.0.0+version: 0.2.0.0 synopsis: Opportunistic hash-consing data structure description: Provides a pure interface for hash-consing values. license: BSD3@@ -12,8 +12,13 @@ library hs-source-dirs: src- exposed-modules: Data.HashCons- Data.HashCons.Internal+ exposed-modules:+ Data.HashCons+ Data.HashCons.IntMap+ Data.HashCons.IntMap.Internal+ Data.HashCons.Internal+ Data.HashCons.WordMap+ Data.HashCons.WordMap.Internal build-depends: base >=4.9 && <5, hashable >= 1.4.0 && < 1.5 default-language: Haskell2010@@ -22,6 +27,9 @@ type: exitcode-stdio-1.0 hs-source-dirs: test main-is: Main.hs+ other-modules:+ Data.HashCons.Internal.Test+ Data.HashCons.IntMap.Test build-depends: base >=4.9 && <5, async, hash-cons,
src/Data/HashCons.hs view
@@ -1,7 +1,9 @@+{-# LANGUAGE PatternSynonyms #-} module Data.HashCons (module X) where import Data.HashCons.Internal as X ( HashCons+ , pattern HashCons , hashCons , unHashCons )
+ src/Data/HashCons/IntMap.hs view
@@ -0,0 +1,105 @@+-- | This module provides an efficient implementation of maps from 'Int' keys to values.+-- It is a thin wrapper around 'Data.HashCons.WordMap', converting 'Int' keys to 'Word'.++module Data.HashCons.IntMap+ ( IntMap+ , empty+ , singleton+ , singletonNonEmpty+ , insert+ , insertNonEmpty+ , lookup+ , lookupNonEmpty+ , map+ , mapWithKey+ , traverseWithKey+ , union+ , unionNonEmpty+ , intersection+ , intersectionNonEmpty+ , difference+ , differenceNonEmpty+ , member+ ) where++import Prelude hiding (lookup, map)+import Data.HashCons.WordMap (WordMap, NonEmptyWordMap)+import qualified Data.HashCons.WordMap as WordMap+import Data.Word+import Data.Hashable++newtype IntMap a = IntMap { unWordMap :: WordMap a }++newtype NonEmptyIntMap a = NonEmptyIntMap { unNonEmptyWordMap :: NonEmptyWordMap a }++-- | /O(1)/. Create an empty map.+empty :: IntMap a+empty = IntMap WordMap.empty++-- | /O(1)/. Create a map with a single key-value pair.+singleton :: Hashable a => Int -> a -> IntMap a+singleton k v = IntMap $ WordMap.singleton (fromIntegral k) v++-- | /O(1)/. Create a map with a single key-value pair.+singletonNonEmpty :: Hashable a => Int -> a -> NonEmptyIntMap a+singletonNonEmpty k v = NonEmptyIntMap $ WordMap.singletonNonEmpty (fromIntegral k) v++-- | /O(log n)/. Insert a key-value pair into the map.+insert :: Hashable a => Int -> a -> IntMap a -> IntMap a+insert k v (IntMap m) = IntMap $ WordMap.insert (fromIntegral k) v m++-- | /O(log n)/. Insert a key-value pair into the map.+insertNonEmpty :: Hashable a => Int -> a -> NonEmptyIntMap a -> NonEmptyIntMap a+insertNonEmpty k v (NonEmptyIntMap m) = NonEmptyIntMap $ WordMap.insertNonEmpty (fromIntegral k) v m++-- | /O(log n)/. Lookup the value at a key in the map.+lookup :: Int -> IntMap a -> Maybe a+lookup k (IntMap m) = WordMap.lookup (fromIntegral k) m++-- | /O(log n)/. Lookup the value at a key in the map.+lookupNonEmpty :: Int -> NonEmptyIntMap a -> Maybe a+lookupNonEmpty k (NonEmptyIntMap m) = WordMap.lookupNonEmpty (fromIntegral k) m++-- | /O(n)/. Map a function over all values in the map.+map :: Hashable b => (a -> b) -> IntMap a -> IntMap b+map f (IntMap m) = IntMap $ WordMap.map f m++-- | /O(n)/. Map a function over all key-value pairs in the map.+mapWithKey :: Hashable b => (Int -> a -> b) -> IntMap a -> IntMap b+mapWithKey f (IntMap m) = IntMap $ WordMap.mapWithKey (\k v -> f (fromIntegral k) v) m++-- | /O(n)/. Traverse the map with a function that accesses keys.+traverseWithKey :: (Applicative t, Hashable b) => (Int -> a -> t b) -> IntMap a -> t (IntMap b)+traverseWithKey f (IntMap m) = IntMap <$> WordMap.traverseWithKey (\k v -> f (fromIntegral k) v) m++-- | /O(n)/. The union of two maps. If a key occurs in both maps, the value from the left map is used.+union :: Hashable a => IntMap a -> IntMap a -> IntMap a+union (IntMap m1) (IntMap m2) = IntMap $ WordMap.union m1 m2++-- | /O(n)/. The union of two maps. If a key occurs in both maps, the value from the left map is used.+unionNonEmpty :: Hashable a => NonEmptyIntMap a -> IntMap a -> NonEmptyIntMap a+unionNonEmpty (NonEmptyIntMap m1) (IntMap m2) = NonEmptyIntMap $ WordMap.unionNonEmpty m1 m2++-- | /O(n)/. Intersection of two maps. Only keys present in both maps are included.+intersection :: Hashable a => IntMap a -> IntMap a -> IntMap a+intersection (IntMap m1) (IntMap m2) = IntMap $ WordMap.intersection m1 m2++-- | /O(n)/. Intersection of two maps. Only keys present in both maps are included.+intersectionNonEmpty :: Hashable a => NonEmptyIntMap a -> IntMap a -> IntMap a+intersectionNonEmpty (NonEmptyIntMap m1) (IntMap m2) = IntMap $ WordMap.intersectionNonEmpty m1 m2++-- | /O(n)/. Difference of two maps. Keys in the first map but not in the second are included.+difference :: Hashable a => IntMap a -> IntMap a -> IntMap a+difference (IntMap m1) (IntMap m2) = IntMap $ WordMap.difference m1 m2++-- | /O(n)/. Intersection of two maps. Only keys present in both maps are included.+differenceNonEmpty :: Hashable a => NonEmptyIntMap a -> IntMap a -> IntMap a+differenceNonEmpty (NonEmptyIntMap m1) (IntMap m2) = IntMap $ WordMap.differenceNonEmpty m1 m2++-- | Check if a key is present in the map.+member :: Int -> IntMap a -> Bool+member k (IntMap m) = WordMap.member (fromIntegral k) m++-- | Check if a key is present in the map.+memberNonEmpty :: Int -> NonEmptyIntMap a -> Bool+memberNonEmpty k (NonEmptyIntMap m) = WordMap.memberNonEmpty (fromIntegral k) m
+ src/Data/HashCons/IntMap/Internal.hs view
@@ -0,0 +1,1 @@+module Data.HashCons.IntMap.Internal where
src/Data/HashCons/Internal.hs view
@@ -1,34 +1,48 @@ {-# LANGUAGE MagicHash #-}+{-# LANGUAGE UnboxedTuples #-}+{-# LANGUAGE PatternSynonyms #-}+{-# LANGUAGE ViewPatterns #-}+{-# LANGUAGE ScopedTypeVariables #-} module Data.HashCons.Internal where +import Control.Exception import Control.Monad (when) import Data.Hashable (Hashable, hash, hashWithSalt) import Data.IORef (IORef, newIORef, readIORef, writeIORef)+import GHC.Base (compareInt#, Int#, IO (..), anyToAddr#, addr2Int#)+import GHC.Exts (Any, Addr#, unsafeCoerce#) import System.IO.Unsafe (unsafeDupablePerformIO) import Text.ParserCombinators.ReadPrec (step) import Text.Read (Read(..), lexP, parens, prec) import Text.Read.Lex (Lexeme (Ident))-import GHC.Exts (reallyUnsafePtrEquality#) +import Debug.Trace+ -- | 'HashCons' with a precomputed hash and an 'IORef' to the value. -- -- WARNING: Do not use this type to wrap types whose Eq or Ord instances -- allow distinguishable values to compare as equal; this will result in -- nondeterminism or even visible mutation of semantically-immutable -- values at runtime.-data HashCons a = HashCons+data HashCons a = HashConsC { _hashCons_hash :: {-# UNPACK #-} !Int -- ^ Precomputed hash , _hashCons_ref :: {-# UNPACK #-} !(IORef a) -- ^ Reference to the value } +pattern HashCons :: Hashable a => () => a -> HashCons a+pattern HashCons x <- (unHashCons -> x) where+ HashCons x = hashCons x+ -- | Create a new 'HashCons'. hashCons :: Hashable a => a -> HashCons a-hashCons a = HashCons (hash a) $ unsafeDupablePerformIO $ newIORef a+hashCons a = HashConsC (hash a) $ unsafeDupablePerformIO $ newIORef a+{-# INLINE hashCons #-} -- | Extract the value from a 'HashCons'. unHashCons :: HashCons a -> a-unHashCons (HashCons _ ref) = unsafeDupablePerformIO $ readIORef ref+unHashCons (HashConsC _ ref) = unsafeDupablePerformIO $ readIORef ref+{-# INLINE unHashCons #-} -- | Show instance that displays 'HashCons' in the format "hashCons <x>" instance Show a => Show (HashCons a) where@@ -45,33 +59,66 @@ pure $ hashCons a instance Eq a => Eq (HashCons a) where- HashCons h1 ref1 == HashCons h2 ref2+ HashConsC h1 ref1 == HashConsC h2 ref2 | ref1 == ref2 = True | h1 /= h2 = False- | otherwise = unsafeDupablePerformIO $ do- a1 <- readIORef ref1- a2 <- readIORef ref2- let eq = a1 == a2- when eq $ case reallyUnsafePtrEquality# a1 a2 of- 0# -> writeIORef ref1 a2- _ -> pure ()- pure eq+ | otherwise = compareAndSubstitute ((==) :: a -> a -> Bool) True ref1 ref2+ {- INLINE (==) #-} -- | NOTE: This instance orders by hash first, and only secondarily by -- the 'Ord' instance of 'a', to improve performance. instance Ord a => Ord (HashCons a) where- compare (HashCons h1 ref1) (HashCons h2 ref2) = case compare h1 h2 of+ compare (HashConsC h1 ref1) (HashConsC h2 ref2) = case compare h1 h2 of EQ -> if ref1 == ref2 then EQ- else unsafeDupablePerformIO $ do- a1 <- readIORef ref1- a2 <- readIORef ref2- let result = compare a1 a2- when (result == EQ) $ case reallyUnsafePtrEquality# a1 a2 of- 0# -> writeIORef ref1 a2- _ -> pure ()- pure result+ else compareAndSubstitute compare EQ ref1 ref2 result -> result+ {-# INLINE compare #-} instance Eq a => Hashable (HashCons a) where- hashWithSalt salt (HashCons h _) = hashWithSalt salt h+ hashWithSalt salt (HashConsC h _) = hashWithSalt salt h+ {-# INLINE hashWithSalt #-}++-- Compare the values in the IORefs with the given comparator, and if the result+-- indicates that they are equal, replace one with the other, preferring the one+-- whose pointer is lower. This is not expected to be totally stable, but it+-- should be *somewhat* stable, and should push us in direction of coalescing+-- more values. Without this, if you have a, b, and c, all with equal but+-- distinct values, and compare b == a and b == c repeatedly, but never compare+-- a == c, you could end up with the value of b flapping between that of a and+-- c, costing the worst-case equality check time repeatedly, and never settling+-- on a particular representation of the value. With this, you should settle on+-- a single value unless you get extremely unlucky with the way that addresses+-- move around.+compareAndSubstitute+ :: Eq r+ => (a -> a -> r)+ -> r+ -> IORef a+ -> IORef a+ -> r+compareAndSubstitute cmp eq ref1 ref2 = unsafeDupablePerformIO $ do+ a1 <- readIORef ref1+ a2 <- readIORef ref2+ let result = a1 `cmp` a2+ when (result == eq) $ do+ -- NOTE: These should already be forced by (==), but in the unlikely event+ -- that they are not (i.e. because (==) on their type unconditionally+ -- returns True), we need to ensure they are not thunks, according to the+ -- documentation of anyToAddr#+ evaluate a1+ evaluate a2+ -- NOTE: There is a race condition here: the addresses could change in+ -- between when they are read. However, since either (or neither) swap is+ -- fine, we are OK with this only working "most" of the time (which we+ -- expect to be a very high fraction).+ addrCmpResult <- IO $ \s ->+ case anyToAddr# (unsafeCoerce# a1 :: Any) s of+ (# s', addr1 #) -> case anyToAddr# (unsafeCoerce# a2 :: Any) s' of+ (# s'', addr2 #) -> (# s'', addr2Int# addr1 `compareInt#` addr2Int# addr2 #)+ case addrCmpResult of+ LT -> writeIORef ref2 a1+ GT -> writeIORef ref1 a2+ EQ -> pure ()+ pure result+{-# INLINE compareAndSubstitute #-}
+ src/Data/HashCons/WordMap.hs view
@@ -0,0 +1,29 @@+module Data.HashCons.WordMap+ ( WordMap+ , NonEmptyWordMap+ , empty+ , singleton+ , singletonNonEmpty+ , insert+ , insertNonEmpty+ , lookup+ , lookupNonEmpty+ , map+ , mapNonEmpty+ , mapWithKey+ , mapWithKeyNonEmpty+ , traverseWithKey+ , traverseWithKeyNonEmpty+ , union+ , unionNonEmpty+ , intersection+ , intersectionNonEmpty+ , difference+ , differenceNonEmpty+ , member+ , memberNonEmpty+ ) where++import Prelude ()++import Data.HashCons.WordMap.Internal
+ src/Data/HashCons/WordMap/Internal.hs view
@@ -0,0 +1,286 @@+{-# LANGUAGE DeriveFunctor, DeriveFoldable, DeriveTraversable #-}+{-# LANGUAGE PatternSynonyms #-}+{-# LANGUAGE DeriveGeneric #-}++-- | This module provides an efficient implementation of maps from 'Word' keys to values.+-- It uses a Patricia trie with hash-consing to enable fast structural sharing and+-- O(1) equality checks. The 'HashCons' type wraps the internal nodes to take advantage+-- of O(1) 'Eq' and 'Ord' operations provided by hash-consing.+--+-- Note: This is an internal module. Users should prefer the public interface provided+-- by 'Data.HashCons.WordMap'.++module Data.HashCons.WordMap.Internal where++import Prelude hiding (lookup, map)+import Data.Bits+import Data.Hashable+import Data.HashCons+import GHC.Generics++-- | The main 'WordMap' data type.+data WordMap a+ = Empty+ | NonEmptyMap !(HashCons (NonEmptyWordMap a))+ deriving (Show, Eq, Ord, Generic)++instance Hashable a => Hashable (WordMap a)++-- | Non-empty trie nodes.+data NonEmptyWordMap a+ = Leaf !Word a+ | Bin !Word !Word (WordMap a) (WordMap a)+ deriving (Show, Eq, Ord, Generic)++instance Hashable a => Hashable (NonEmptyWordMap a)++-- | /O(1)/. Create an empty map.+empty :: WordMap a+empty = Empty++-- | /O(1)/. Create a map with a single key-value pair.+singleton :: Hashable a => Word -> a -> WordMap a+singleton k v = NonEmptyMap (hashCons (Leaf k v))++-- | /O(1)/. Create a map with a single key-value pair.+singletonNonEmpty :: Hashable a => Word -> a -> NonEmptyWordMap a+singletonNonEmpty = Leaf++-- | /O(log n)/. Insert a key-value pair into the map.+-- If the key is already present, the value is replaced.+insert :: Hashable a => Word -> a -> WordMap a -> WordMap a+insert k v Empty = singleton k v+insert k v (NonEmptyMap hc) =+ let t = unHashCons hc+ t' = insertNonEmpty k v t+ in if t' == t -- Exploit O(1) 'Eq' from 'HashCons'+ then NonEmptyMap hc -- No change needed+ else NonEmptyMap (hashCons t')++insertNonEmpty :: Hashable a => Word -> a -> NonEmptyWordMap a -> NonEmptyWordMap a+insertNonEmpty k v t@(Leaf k' _)+ | k == k' = Leaf k v -- Replace the existing value+ | otherwise = join k (Leaf k v) k' t+insertNonEmpty k v t@(Bin p m l r)+ | match k p m = if zero k m+ then+ let l' = insert k v l+ in if l' == l then t else Bin p m l' r+ else+ let r' = insert k v r+ in if r' == r then t else Bin p m l r'+ | otherwise = join k (Leaf k v) p t++-- | /O(log n)/. Lookup the value at a key in the map.+lookup :: Word -> WordMap a -> Maybe a+lookup _ Empty = Nothing+lookup k (NonEmptyMap hc) = lookupNonEmpty k (unHashCons hc)++lookupNonEmpty :: Word -> NonEmptyWordMap a -> Maybe a+lookupNonEmpty k (Leaf k' v)+ | k == k' = Just v+ | otherwise = Nothing+lookupNonEmpty k (Bin p m l r)+ | match k p m = if zero k m then lookup k l else lookup k r+ | otherwise = Nothing++-- | /O(n)/. Map a function over all values in the map.+map :: Hashable b => (a -> b) -> WordMap a -> WordMap b+map _ Empty = Empty+map f (NonEmptyMap hc) = NonEmptyMap (hashCons (mapNonEmpty f (unHashCons hc)))++mapNonEmpty :: Hashable b => (a -> b) -> NonEmptyWordMap a -> NonEmptyWordMap b+mapNonEmpty f (Leaf k v) = Leaf k (f v)+mapNonEmpty f (Bin p m l r) = Bin p m (map f l) (map f r)++-- | /O(n)/. Map a function over all key-value pairs in the map.+mapWithKey :: Hashable b => (Word -> a -> b) -> WordMap a -> WordMap b+mapWithKey _ Empty = Empty+mapWithKey f (NonEmptyMap hc) = NonEmptyMap (hashCons (mapWithKeyNonEmpty f (unHashCons hc)))++mapWithKeyNonEmpty :: Hashable b => (Word -> a -> b) -> NonEmptyWordMap a -> NonEmptyWordMap b+mapWithKeyNonEmpty f (Leaf k v) = Leaf k (f k v)+mapWithKeyNonEmpty f (Bin p m l r) = Bin p m (mapWithKey f l) (mapWithKey f r)++-- | /O(n)/. Traverse the map with a function that accesses keys.+traverseWithKey :: (Applicative t, Hashable b) => (Word -> a -> t b) -> WordMap a -> t (WordMap b)+traverseWithKey _ Empty = pure Empty+traverseWithKey f (NonEmptyMap hc) = NonEmptyMap . hashCons <$> traverseWithKeyNonEmpty f (unHashCons hc)++traverseWithKeyNonEmpty :: (Applicative t, Hashable b) => (Word -> a -> t b) -> NonEmptyWordMap a -> t (NonEmptyWordMap b)+traverseWithKeyNonEmpty f (Leaf k v) = Leaf k <$> f k v+traverseWithKeyNonEmpty f (Bin p m l r) = Bin p m <$> traverseWithKey f l <*> traverseWithKey f r++-- | /O(n)/. The union of two maps. If a key occurs in both maps, the value from the left map is used.+union :: Hashable a => WordMap a -> WordMap a -> WordMap a+union t1 Empty = t1+union Empty t2 = t2+union t1@(NonEmptyMap hc1) t2@(NonEmptyMap hc2)+ | hc1 == hc2 = t1 -- O(1) comparison via 'HashCons'+union (NonEmptyMap hc1) t2 =+ NonEmptyMap (hashCons (unionNonEmpty (unHashCons hc1) t2))++unionNonEmpty :: Hashable a => NonEmptyWordMap a -> WordMap a -> NonEmptyWordMap a+unionNonEmpty t1 Empty = t1+unionNonEmpty t1 (NonEmptyMap hc2) = unionNonEmptyNonEmpty t1 (unHashCons hc2)++unionNonEmptyNonEmpty :: Hashable a => NonEmptyWordMap a -> NonEmptyWordMap a -> NonEmptyWordMap a+unionNonEmptyNonEmpty t1 t2+ | t1 == t2 = t1 -- O(1) comparison via 'HashCons'+unionNonEmptyNonEmpty t1@(Leaf k1 v1) t2@(Leaf k2 _)+ | k1 == k2 = t1 -- Prefer left map's value+ | otherwise = join k1 t1 k2 t2+unionNonEmptyNonEmpty t1@(Leaf k1 v1) t2@(Bin p2 m2 l2 r2)+ | match k1 p2 m2 = if zero k1 m2+ then Bin p2 m2 (union (singleton k1 v1) l2) r2+ else Bin p2 m2 l2 (union (singleton k1 v1) r2)+ | otherwise = join k1 t1 p2 t2+unionNonEmptyNonEmpty t1@(Bin p1 m1 l1 r1) t2@(Leaf k2 v2)+ | match k2 p1 m1 = if zero k2 m1+ then Bin p1 m1 (union l1 (singleton k2 v2)) r1+ else Bin p1 m1 l1 (union r1 (singleton k2 v2))+ | otherwise = join p1 t1 k2 t2+unionNonEmptyNonEmpty t1@(Bin p1 m1 l1 r1) t2@(Bin p2 m2 l2 r2)+ | t1 == t2 = t1 -- O(1) comparison via 'HashCons'+ | shorter m1 m2 =+ if match p2 p1 m1+ then if zero p2 m1+ then Bin p1 m1 (union l1 (NonEmptyMap (hashCons t2))) r1+ else Bin p1 m1 l1 (union r1 (NonEmptyMap (hashCons t2)))+ else join p1 t1 p2 t2+ | shorter m2 m1 =+ if match p1 p2 m2+ then if zero p1 m2+ then Bin p2 m2 (union (NonEmptyMap (hashCons t1)) l2) r2+ else Bin p2 m2 l2 (union (NonEmptyMap (hashCons t1)) r2)+ else join p1 t1 p2 t2+ | p1 == p2 = Bin p1 m1 (union l1 l2) (union r1 r2)+ | otherwise = join p1 t1 p2 t2++-- | /O(n)/. Intersection of two maps. Only keys present in both maps are included.+intersection :: Hashable a => WordMap a -> WordMap a -> WordMap a+intersection Empty _ = Empty+intersection _ Empty = Empty+intersection t1@(NonEmptyMap hc1) t2@(NonEmptyMap hc2)+ | hc1 == hc2 = t1 -- O(1) comparison via 'HashCons'+intersection (NonEmptyMap hc1) t2 =+ intersectionNonEmpty (unHashCons hc1) t2++intersectionNonEmpty :: Hashable a => NonEmptyWordMap a -> WordMap a -> WordMap a+intersectionNonEmpty _ Empty = Empty+intersectionNonEmpty t1 (NonEmptyMap hc2) =+ case t1 of+ Leaf k1 v1 -> if member k1 (NonEmptyMap hc2)+ then singleton k1 v1+ else Empty+ Bin p1 m1 l1 r1 -> case unHashCons hc2 of+ Leaf k2 a2+ | match k2 p1 m1 -> if zero k2 m1+ then intersection l1 (singleton k2 a2)+ else intersection r1 (singleton k2 a2)+ | otherwise -> Empty+ Bin p2 m2 l2 r2+ | shorter m1 m2 ->+ if match p2 p1 m1+ then if zero p2 m1+ then intersection l1 (NonEmptyMap hc2)+ else intersection r1 (NonEmptyMap hc2)+ else Empty+ | shorter m2 m1 ->+ if match p1 p2 m2+ then if zero p1 m2+ then intersection (NonEmptyMap (hashCons t1)) l2+ else intersection (NonEmptyMap (hashCons t1)) r2+ else Empty+ | p1 == p2 -> let l = intersection l1 l2+ r = intersection r1 r2+ in combine p1 m1 l r+ | otherwise -> Empty++-- | /O(n)/. Difference of two maps. Keys in the first map but not in the second are included.+difference :: Hashable a => WordMap a -> WordMap a -> WordMap a+difference Empty _ = Empty+difference t1 Empty = t1+difference t1@(NonEmptyMap hc1) t2@(NonEmptyMap hc2)+ | hc1 == hc2 = Empty -- O(1) comparison via 'HashCons'+difference (NonEmptyMap hc1) t2 =+ differenceNonEmpty (unHashCons hc1) t2++differenceNonEmpty :: Hashable a => NonEmptyWordMap a -> WordMap a -> WordMap a+differenceNonEmpty t1 Empty = NonEmptyMap (hashCons t1)+differenceNonEmpty t1 (NonEmptyMap hc2) =+ case t1 of+ Leaf k1 v1 -> if member k1 (NonEmptyMap hc2)+ then Empty+ else singleton k1 v1+ Bin p1 m1 l1 r1 -> case unHashCons hc2 of+ Leaf k2 a2+ | match k2 p1 m1 -> if zero k2 m1+ then combine p1 m1 (difference l1 (singleton k2 a2)) r1+ else combine p1 m1 l1 (difference r1 (singleton k2 a2))+ | otherwise -> NonEmptyMap (hashCons t1)+ Bin p2 m2 l2 r2+ | shorter m1 m2 ->+ if match p2 p1 m1+ then if zero p2 m1+ then combine p1 m1 (difference l1 (NonEmptyMap hc2)) r1+ else combine p1 m1 l1 (difference r1 (NonEmptyMap hc2))+ else NonEmptyMap (hashCons t1)+ | shorter m2 m1 ->+ if match p1 p2 m2+ then if zero p1 m2+ then difference (NonEmptyMap (hashCons t1)) l2+ else difference (NonEmptyMap (hashCons t1)) r2+ else NonEmptyMap (hashCons t1)+ | p1 == p2 -> let l = difference l1 l2+ r = difference r1 r2+ in combine p1 m1 l r+ | otherwise -> NonEmptyMap (hashCons t1)++-- | Check if a key is present in the map.+member :: Word -> WordMap a -> Bool+member _ Empty = False+member k (NonEmptyMap hc) = memberNonEmpty k (unHashCons hc)++memberNonEmpty :: Word -> NonEmptyWordMap a -> Bool+memberNonEmpty k (Leaf k' _)+ | k == k' = True+ | otherwise = False+memberNonEmpty k (Bin p m l r)+ | match k p m = if zero k m then member k l else member k r+ | otherwise = False++-- | Helper function to combine two subtrees.+combine :: Hashable a => Word -> Word -> WordMap a -> WordMap a -> WordMap a+combine _ _ Empty Empty = Empty+combine p m l r = NonEmptyMap (hashCons (Bin p m l r))++-- | Helper functions.++join :: Hashable a => Word -> NonEmptyWordMap a -> Word -> NonEmptyWordMap a -> NonEmptyWordMap a+join p1 t1 p2 t2 =+ let m = branchingBit p1 p2+ p = mask p1 m+ in if zero p1 m+ then Bin p m (NonEmptyMap (hashCons t1)) (NonEmptyMap (hashCons t2))+ else Bin p m (NonEmptyMap (hashCons t2)) (NonEmptyMap (hashCons t1))++match :: Word -> Word -> Word -> Bool+match k p m = mask k m == p++mask :: Word -> Word -> Word+mask k m = k .&. (complement (m - 1) `xor` m)++zero :: Word -> Word -> Bool+zero k m = (k .&. m) == 0++branchingBit :: Word -> Word -> Word+branchingBit p1 p2 = highestBitMask (p1 `xor` p2)++highestBitMask :: Word -> Word+highestBitMask x+ | x == 0 = 0+ | otherwise = bit (finiteBitSize x - 1 - countLeadingZeros x)++shorter :: Word -> Word -> Bool+shorter m1 m2 = m1 > m2
+ test/Data/HashCons/IntMap/Test.hs view
@@ -0,0 +1,63 @@+module Data.HashCons.IntMap.Test (tests) where++import Prelude hiding (lookup)+import Test.Tasty+import Test.Tasty.QuickCheck as QC+import Data.Maybe (isJust, isNothing)+import Data.HashCons.IntMap+import Data.Hashable++tests :: TestTree+tests = testGroup "Data.HashCons.IntMap"+ [ QC.testProperty "Insert and Lookup" prop_insertLookup+ , QC.testProperty "Insert overwrites existing key" prop_insertOverwrite+ , QC.testProperty "Insert preserves other keys" prop_insertPreservesOtherKeys+ , QC.testProperty "Union contains all keys" prop_unionContainsAllKeys+ , QC.testProperty "Intersection contains only common keys" prop_intersectionCommonKeys+ , QC.testProperty "Difference removes keys" prop_differenceRemovesKeys+ ]++-- Property: Inserting a key and then looking it up gives back the value+prop_insertLookup :: Int -> Int -> Bool+prop_insertLookup k v =+ lookup k (insert k v empty) == Just v++-- Property: Inserting a key overwrites the existing value+prop_insertOverwrite :: Int -> Int -> Int -> Bool+prop_insertOverwrite k v1 v2 =+ lookup k (insert k v2 (insert k v1 empty)) == Just v2++-- Property: Inserting a key does not affect other keys+prop_insertPreservesOtherKeys :: Int -> Int -> Int -> Int -> Property+prop_insertPreservesOtherKeys k1 v1 k2 v2 =+ k1 /= k2 ==> lookup k1 (insert k2 v2 (insert k1 v1 empty)) == Just v1++-- Property: Union contains all keys from both maps+prop_unionContainsAllKeys :: [(Int, Int)] -> [(Int, Int)] -> Int -> Bool+prop_unionContainsAllKeys kvs1 kvs2 k =+ let m1 = fromList kvs1+ m2 = fromList kvs2+ mu = union m1 m2+ in lookup k mu == case lookup k m1 of+ Just v -> Just v+ Nothing -> lookup k m2++-- Property: Intersection contains only keys common to both maps+prop_intersectionCommonKeys :: [(Int, Int)] -> [(Int, Int)] -> Int -> Bool+prop_intersectionCommonKeys kvs1 kvs2 k =+ let m1 = fromList kvs1+ m2 = fromList kvs2+ mi = intersection m1 m2+ in lookup k mi == if isJust (lookup k m1) && isJust (lookup k m2) then lookup k m1 else Nothing++-- Property: Difference removes keys present in the second map from the first map+prop_differenceRemovesKeys :: [(Int, Int)] -> [(Int, Int)] -> Int -> Bool+prop_differenceRemovesKeys kvs1 kvs2 k =+ let m1 = fromList kvs1+ m2 = fromList kvs2+ md = difference m1 m2+ in lookup k md == if isJust (lookup k m1) && isNothing (lookup k m2) then lookup k m1 else Nothing++-- Helper function to create a map from a list of key-value pairs+fromList :: Hashable a => [(Int, a)] -> IntMap a+fromList = foldr (\(k,v) m -> insert k v m) empty
+ test/Data/HashCons/Internal/Test.hs view
@@ -0,0 +1,71 @@+module Data.HashCons.Internal.Test (tests) where++import Control.Concurrent.Async+import Control.Monad+import Data.HashCons.Internal+import Data.Hashable+import Data.IORef+import System.Mem.StableName+import Test.Tasty+import Test.Tasty.HUnit+import Test.Tasty.QuickCheck as QC++tests :: TestTree+tests = testGroup "Data.HashCons.Internal"+ [ QC.testProperty "Reflexivity" prop_reflexivity+ , QC.testProperty "Symmetry" prop_symmetry+ , QC.testProperty "Consistency of unHashCons" prop_consistency_unHashCons+ , QC.testProperty "Deduplication" prop_deduplication+ , QC.testProperty "Ord and Eq Consistency" prop_ord_eq_consistency+ , QC.testProperty "Ord Transitivity" prop_ord_transitivity+ ]++prop_reflexivity :: Int -> Property+prop_reflexivity x = let hx = hashCons x in hx === hx++prop_symmetry :: Int -> Int -> Property+prop_symmetry x y = let hx = hashCons x; hy = hashCons y in (hx == hy) === (hy == hx)++prop_consistency_unHashCons :: Int -> Int -> Property+prop_consistency_unHashCons x y = let hx = hashCons x; hy = hashCons y in+ (hx == hy) === (unHashCons hx == unHashCons hy)++prop_deduplication :: Int -> Int -> Property+prop_deduplication x y = ioProperty $ do+ let hx = hashCons x+ hy = hashCons y+ -- Perform the equality check to trigger deduplication+ areEqual <- eqHashConsIO hx hy+ if areEqual+ then do+ -- Read the contents of both IORefs+ a1 <- readIORef (_hashCons_ref hx)+ a2 <- readIORef (_hashCons_ref hy)+ -- Create StableNames for both contents+ sn1 <- makeStableName a1+ sn2 <- makeStableName a2+ -- Check if the StableNames are equal (pointer equality)+ return $ sn1 == sn2+ else return True -- If not equal, deduplication isn't expected++-- Helper function to perform equality check in IO+eqHashConsIO :: Eq a => HashCons a -> HashCons a -> IO Bool+eqHashConsIO (HashConsC h1 ref1) (HashConsC h2 ref2)+ | h1 /= h2 = return False+ | ref1 == ref2 = return True+ | otherwise = do+ a1 <- readIORef ref1+ a2 <- readIORef ref2+ if a1 == a2+ then do+ writeIORef ref1 a2+ return True+ else return False++prop_ord_eq_consistency :: Int -> Int -> Bool+prop_ord_eq_consistency x y = let hx = hashCons x; hy = hashCons y in+ (hx == hy) == (compare hx hy == EQ)++prop_ord_transitivity :: Int -> Int -> Int -> Property+prop_ord_transitivity x y z = let hx = hashCons x; hy = hashCons y; hz = hashCons z in+ (compare hx hy == LT && compare hy hz == LT) ==> (compare hx hz == LT)
test/Main.hs view
@@ -1,74 +1,14 @@ module Main (main) where -import Control.Concurrent.Async-import Control.Monad-import Data.HashCons.Internal-import Data.Hashable-import Data.IORef-import System.Mem.StableName import Test.Tasty-import Test.Tasty.HUnit-import Test.Tasty.QuickCheck as QC+import qualified Data.HashCons.Internal.Test+import qualified Data.HashCons.IntMap.Test main :: IO () main = defaultMain tests tests :: TestTree-tests = testGroup "HashCons Tests"- [ QC.testProperty "Reflexivity" prop_reflexivity- , QC.testProperty "Symmetry" prop_symmetry- , QC.testProperty "Consistency of unHashCons" prop_consistency_unHashCons- , QC.testProperty "Deduplication" prop_deduplication- , QC.testProperty "Ord and Eq Consistency" prop_ord_eq_consistency- , QC.testProperty "Ord Transitivity" prop_ord_transitivity+tests = testGroup "hash-cons"+ [ Data.HashCons.Internal.Test.tests+ , Data.HashCons.IntMap.Test.tests ]--prop_reflexivity :: Int -> Property-prop_reflexivity x = let hx = hashCons x in hx === hx--prop_symmetry :: Int -> Int -> Property-prop_symmetry x y = let hx = hashCons x; hy = hashCons y in (hx == hy) === (hy == hx)--prop_consistency_unHashCons :: Int -> Int -> Property-prop_consistency_unHashCons x y = let hx = hashCons x; hy = hashCons y in- (hx == hy) === (unHashCons hx == unHashCons hy)--prop_deduplication :: Int -> Int -> Property-prop_deduplication x y = ioProperty $ do- let hx = hashCons x- hy = hashCons y- -- Perform the equality check to trigger deduplication- areEqual <- eqHashConsIO hx hy- if areEqual- then do- -- Read the contents of both IORefs- a1 <- readIORef (_hashCons_ref hx)- a2 <- readIORef (_hashCons_ref hy)- -- Create StableNames for both contents- sn1 <- makeStableName a1- sn2 <- makeStableName a2- -- Check if the StableNames are equal (pointer equality)- return $ sn1 == sn2- else return True -- If not equal, deduplication isn't expected---- Helper function to perform equality check in IO-eqHashConsIO :: Eq a => HashCons a -> HashCons a -> IO Bool-eqHashConsIO (HashCons h1 ref1) (HashCons h2 ref2)- | h1 /= h2 = return False- | ref1 == ref2 = return True- | otherwise = do- a1 <- readIORef ref1- a2 <- readIORef ref2- if a1 == a2- then do- writeIORef ref1 a2- return True- else return False--prop_ord_eq_consistency :: Int -> Int -> Bool-prop_ord_eq_consistency x y = let hx = hashCons x; hy = hashCons y in- (hx == hy) == (compare hx hy == EQ)--prop_ord_transitivity :: Int -> Int -> Int -> Property-prop_ord_transitivity x y z = let hx = hashCons x; hy = hashCons y; hz = hashCons z in- (compare hx hy == LT && compare hy hz == LT) ==> (compare hx hz == LT)