perfect-hash-generator 0.1.0.2 → 0.1.0.3
raw patch · 10 files changed
+203/−185 lines, 10 filesPVP ok
version bump matches the API change (PVP)
API changes (from Hackage documentation)
Files
- demo/Ints/Main.hs +71/−0
- demo/IntsDemo.hs +0/−71
- demo/Strings/Main.hs +33/−0
- demo/StringsDemo.hs +0/−33
- perfect-hash-generator.cabal +8/−8
- src/Data/PerfectHash/Construction.hs +22/−12
- src/Data/PerfectHash/Hashing.hs +8/−0
- src/Data/PerfectHash/Lookup.hs +2/−2
- test/Main.hs +59/−0
- test/Test.hs +0/−59
+ demo/Ints/Main.hs view
@@ -0,0 +1,71 @@+module Main where++import System.Random (RandomGen, mkStdGen, randomR)++import Data.IntSet (IntSet)+import qualified Data.IntSet as IntSet+import qualified Data.PerfectHash.Construction as Construction+import qualified Data.PerfectHash.Hashing as Hashing+import qualified Data.PerfectHash.Lookup as Lookup+import qualified Data.Vector.Unboxed as Vector+import Exercise (Atom (Atom))+import qualified Exercise+++valueCount = 500000++randomRange = (0, Hashing.mask32bits)+++data RandIntAccum t = RandIntAccum+ t -- ^ random number generator+ Int -- ^ max count+ IntSet -- ^ accumulated unique random numbers+++-- | Since computing the size of the set is O(N), we+-- maintain the count separately.+getUniqueRandomIntegers :: RandomGen t => RandIntAccum t -> IntSet+getUniqueRandomIntegers (RandIntAccum std_gen count current_set) =++ if count == 0+ then current_set+ else getUniqueRandomIntegers newstate++ where+ (next_int, next_std_gen) = randomR randomRange std_gen++ a = RandIntAccum next_std_gen+ newstate = if IntSet.member next_int current_set+ then a count current_set+ else a (count - 1) (IntSet.insert next_int current_set)+++intMapTuples :: [(Atom Int, Int)]+intMapTuples = zip (map Atom random_ints) [1..]+ where+ seed_value = RandIntAccum (mkStdGen 0) valueCount IntSet.empty+ random_ints = IntSet.toList $ getUniqueRandomIntegers seed_value+++main = do++ putStrLn $ unwords ["Keys size:", show $ length intMapTuples]++ let lookup_table = Construction.createMinimalPerfectHash intMapTuples++ putStrLn $ unwords [+ "Finished computing lookup table with"+ , show $ Lookup.size lookup_table+ , "entries."+ ]++ let direct_mapping_nonces = Vector.filter (< 0) $ Lookup.nonces lookup_table++ putStrLn $ unwords [+ "There were"+ , show $ Vector.length direct_mapping_nonces+ , "lookup entries with direct mappings."+ ]++ Exercise.eitherExit $ Exercise.testLookups lookup_table intMapTuples
− demo/IntsDemo.hs
@@ -1,71 +0,0 @@-module Main where--import System.Random (RandomGen, mkStdGen, randomR)--import Data.IntSet (IntSet)-import qualified Data.IntSet as IntSet-import qualified Data.PerfectHash.Construction as Construction-import qualified Data.PerfectHash.Hashing as Hashing-import qualified Data.PerfectHash.Lookup as Lookup-import qualified Data.Vector.Unboxed as Vector-import Exercise (Atom (Atom))-import qualified Exercise---valueCount = 500000--randomRange = (0, Hashing.mask32bits)---data RandIntAccum t = RandIntAccum- t -- ^ random number generator- Int -- ^ max count- IntSet -- ^ accumulated unique random numbers----- | Since computing the size of the set is O(N), we--- maintain the count separately.-getUniqueRandomIntegers :: RandomGen t => RandIntAccum t -> IntSet-getUniqueRandomIntegers (RandIntAccum std_gen count current_set) =-- if count == 0- then current_set- else getUniqueRandomIntegers newstate-- where- (next_int, next_std_gen) = randomR randomRange std_gen-- a = RandIntAccum next_std_gen- newstate = if IntSet.member next_int current_set- then a count current_set- else a (count - 1) (IntSet.insert next_int current_set)---intMapTuples :: [(Atom Int, Int)]-intMapTuples = zip (map Atom random_ints) [1..]- where- seed_value = RandIntAccum (mkStdGen 0) valueCount IntSet.empty- random_ints = IntSet.toList $ getUniqueRandomIntegers seed_value---main = do-- putStrLn $ unwords ["Keys size:", show $ length intMapTuples]-- let lookup_table = Construction.createMinimalPerfectHash intMapTuples-- putStrLn $ unwords [- "Finished computing lookup table with"- , show $ Lookup.size lookup_table- , "entries."- ]-- let direct_mapping_nonces = Vector.filter (< 0) $ Lookup.nonces lookup_table-- putStrLn $ unwords [- "There were"- , show $ Vector.length direct_mapping_nonces- , "lookup entries with direct mappings."- ]-- Exercise.eitherExit $ Exercise.testLookups lookup_table intMapTuples
+ demo/Strings/Main.hs view
@@ -0,0 +1,33 @@+module Main where++import Control.Monad (when)++import qualified Data.PerfectHash.Construction as Construction+import qualified Data.PerfectHash.Lookup as Lookup+import qualified Exercise+++enableDebug = False++dictionaryPath = "/usr/share/dict/words"+++main = do++ word_index_tuples <- Exercise.wordsFromFile dictionaryPath++ putStrLn $ unwords ["Words size:", show $ length word_index_tuples]++ let lookup_table = Construction.createMinimalPerfectHash word_index_tuples++ putStrLn $ unwords [+ "Finished computing lookup table with"+ , show $ Lookup.size lookup_table+ , "entries."+ ]++ when enableDebug $ do+ putStrLn $ unwords ["Vector G:", show $ Lookup.nonces lookup_table]+ putStrLn $ unwords ["Vector V:", show $ Lookup.values lookup_table]++ Exercise.eitherExit $ Exercise.testLookups lookup_table word_index_tuples
− demo/StringsDemo.hs
@@ -1,33 +0,0 @@-module Main where--import Control.Monad (when)--import qualified Data.PerfectHash.Construction as Construction-import qualified Data.PerfectHash.Lookup as Lookup-import qualified Exercise---enableDebug = False--dictionaryPath = "/usr/share/dict/words"---main = do-- word_index_tuples <- Exercise.wordsFromFile dictionaryPath-- putStrLn $ unwords ["Words size:", show $ length word_index_tuples]-- let lookup_table = Construction.createMinimalPerfectHash word_index_tuples-- putStrLn $ unwords [- "Finished computing lookup table with"- , show $ Lookup.size lookup_table- , "entries."- ]-- when enableDebug $ do- putStrLn $ unwords ["Vector G:", show $ Lookup.nonces lookup_table]- putStrLn $ unwords ["Vector V:", show $ Lookup.values lookup_table]-- Exercise.eitherExit $ Exercise.testLookups lookup_table word_index_tuples
perfect-hash-generator.cabal view
@@ -3,7 +3,7 @@ -- see: https://github.com/sol/hpack name: perfect-hash-generator-version: 0.1.0.2+version: 0.1.0.3 synopsis: Perfect minimal hashing implementation in native Haskell description: A <https://en.wikipedia.org/wiki/Perfect_hash_function perfect hash function> for a set @S@ is a hash function that maps distinct elements in @S@ to a set of integers, with __no collisions__. A <https://en.wikipedia.org/wiki/Perfect_hash_function#Minimal_perfect_hash_function minimal perfect hash function> is a perfect hash function that maps @n@ keys to @n@ __consecutive__ integers, e.g. the numbers from @0@ to @n-1@. .@@ -71,7 +71,7 @@ default-language: Haskell2010 executable hash-perfectly-ints-demo- main-is: IntsDemo.hs+ main-is: Ints/Main.hs hs-source-dirs: demo test@@ -86,13 +86,13 @@ , hashable , containers other-modules:- StringsDemo+ Strings.Main Exercise- Test+ Main default-language: Haskell2010 executable hash-perfectly-strings-demo- main-is: StringsDemo.hs+ main-is: Strings/Main.hs hs-source-dirs: demo test@@ -106,14 +106,14 @@ , vector , hashable other-modules:- IntsDemo+ Ints.Main Exercise- Test+ Main default-language: Haskell2010 test-suite regression-tests type: exitcode-stdio-1.0- main-is: Test.hs+ main-is: Main.hs hs-source-dirs: test ghc-options: -fwarn-tabs -W
src/Data/PerfectHash/Construction.hs view
@@ -54,6 +54,10 @@ a2 = Vector.generate size (\z -> HashMap.lookupDefault getDefault z $ vals x) +-- | Computes a slot in the destination array (Data.PerfectHash.Lookup.values)+-- for every element in this multi-entry bucket, for the given nonce.+--+-- Return a Nothing for a slot if it collides. attemptNonceRecursive :: (Foldable f, Hashing.ToNumeric a) => HashMapAndSize Int b -> Int@@ -61,26 +65,29 @@ -> [f a] -> [Maybe Int] attemptNonceRecursive _ _ _ [] = []-attemptNonceRecursive values_and_size nonce previous_slots (x:xs) =+attemptNonceRecursive values_and_size nonce occupied_slots (x:xs) = if cannot_use_slot- then [Nothing]+ then pure Nothing else Just slot : recursive_result where HashMapAndSize values size = values_and_size- slot = Hashing.hash nonce x `mod` size+ slot = Hashing.hashToSlot nonce x size - cannot_use_slot = IntSet.member slot previous_slots || HashMap.member slot values+ cannot_use_slot = IntSet.member slot occupied_slots || HashMap.member slot values - next_slots = IntSet.insert slot previous_slots- recursive_result = attemptNonceRecursive values_and_size nonce next_slots xs+ recursive_result = attemptNonceRecursive+ values_and_size+ nonce+ (IntSet.insert slot occupied_slots)+ xs -- | Repeatedly try different values of the nonce until we find a hash function--- that places all items in the bucket into free slots+-- that places all items in the bucket into free slots. ----- Keeps trying forever.+-- Keeps trying forever, incrementing the candidate nonce by @1@ each time. -- Theoretically we're guaranteed to eventually find a solution. findNonceForBucket :: (Foldable f, Hashing.ToNumeric a) => Int@@ -99,6 +106,9 @@ bucket +-- | Searches for a nonce for this bucket, starting with the value @1@,+-- until one is found that results in no collisions for both this bucket+-- and all previous buckets. handleMultiBuckets :: (Foldable f, Hashing.ToNumeric a, Eq (f a), Hashable (f a)) => HashMapAndSize (f a) b -> (Int, [f a])@@ -110,9 +120,9 @@ HashMapAndSize words_dict size = sized_words_dict sized_vals_dict = HashMapAndSize (vals old_lookup_table) size- (slots, d) = findNonceForBucket 1 sized_vals_dict bucket+ (slots, nonce) = findNonceForBucket 1 sized_vals_dict bucket - new_g = HashMap.insert computed_hash d (redirs old_lookup_table)+ new_g = HashMap.insert computed_hash nonce (redirs old_lookup_table) new_values = foldr fold_func (vals old_lookup_table) $ zip [0..] bucket fold_func (i, bucket_val) = HashMap.insert (slots !! i) $@@ -150,7 +160,7 @@ sortOn (negate . length . snd) bucket_hash_tuples where size = HashMap.size words_dict- slot_key_pairs = deriveTuples ((`mod` size) . Hashing.hash 0) $ HashMap.keys words_dict+ slot_key_pairs = deriveTuples (\k -> Hashing.hashToSlot 0 k size) $ HashMap.keys words_dict bucket_hash_tuples = HashMap.toList $ binTuplesBySecond slot_key_pairs @@ -192,7 +202,7 @@ -- * Utilities --- | Place the second elements of the tuples into bins according to the second+-- | Place the first elements of the tuples into bins according to the second -- element. binTuplesBySecond :: (Eq b, Hashable b) => [(a, b)] -> HashMap.HashMap b [a] binTuplesBySecond = foldr f HashMap.empty
src/Data/PerfectHash/Hashing.hs view
@@ -29,6 +29,14 @@ toNum = id +hashToSlot :: (Foldable f, ToNumeric a) =>+ Int -- ^ nonce+ -> f a -- ^ key+ -> Int -- ^ array size+ -> Int+hashToSlot nonce key size = hash nonce key `mod` size++ -- | Uses the \"FNV-1a\" algorithm from the -- <http://isthe.com/chongo/tech/comp/fnv/ FNV website>: --
src/Data/PerfectHash/Lookup.hs view
@@ -79,10 +79,10 @@ where table_size = size lookup_table - nonce_index = Hashing.hash 0 key `mod` table_size+ nonce_index = Hashing.hashToSlot 0 key table_size nonce = nonces lookup_table ! nonce_index -- Negative value indicates that we don't need extra lookup layer v_key = if nonce < 0 then encodeDirectEntry nonce- else Hashing.hash nonce key `mod` table_size+ else Hashing.hashToSlot nonce key table_size
+ test/Main.hs view
@@ -0,0 +1,59 @@+module Main where++import Data.Either (isRight)+import Data.Hashable (Hashable)+import qualified Data.Vector.Unboxed as Vector+import Test.Framework (defaultMain, testGroup)+import Test.Framework.Providers.HUnit (testCase)+import Test.HUnit (assertBool, assertEqual)++import qualified Data.PerfectHash.Construction as Construction+import qualified Data.PerfectHash.Hashing as Hashing+import Exercise (Atom (Atom))+import qualified Exercise+++testHashComputation :: String -> Int -> IO ()+testHashComputation key val =+ assertEqual error_message val computed_hash+ where+ error_message = unwords ["Incorrect hash computation of", key]+ computed_hash = Hashing.hash 0 key+++wordIndexTuples = [+ ("apple", 1 :: Int)+ , ("banana", 2)+ , ("carrot", 3)+ ]+++intMapTuples :: [(Atom Int, Int)]+intMapTuples = [+ (Atom 1000, 1)+ , (Atom 5555, 2)+ , (Atom 9876, 3)+ ]+++testHashLookups :: (Show (f a), Show b, Eq b, Vector.Unbox b, Construction.Defaultable b, Foldable f, Hashing.ToNumeric a, Eq (f a), Hashable (f a)) =>+ [(f a, b)] -> IO ()+testHashLookups word_index_tuples =+ assertBool "Perfect hash lookups failed to match the input" $ isRight test_result_either+ where+ lookup_table = Construction.createMinimalPerfectHash word_index_tuples+ test_result_either = Exercise.testLookups lookup_table word_index_tuples+++tests = [+ testGroup "Hash computation" [+ testCase "compute-hash1" $ testHashComputation "blarg" 3322346319+ ]+ , testGroup "Hash lookups" [+ testCase "word-lookups" $ testHashLookups wordIndexTuples+ , testCase "int-lookups" $ testHashLookups intMapTuples+ ]+ ]+++main = defaultMain tests
− test/Test.hs
@@ -1,59 +0,0 @@-module Main where--import Data.Either (isRight)-import Data.Hashable (Hashable)-import qualified Data.Vector.Unboxed as Vector-import Test.Framework (defaultMain, testGroup)-import Test.Framework.Providers.HUnit (testCase)-import Test.HUnit (assertBool, assertEqual)--import qualified Data.PerfectHash.Construction as Construction-import qualified Data.PerfectHash.Hashing as Hashing-import Exercise (Atom (Atom))-import qualified Exercise---testHashComputation :: String -> Int -> IO ()-testHashComputation key val =- assertEqual error_message val computed_hash- where- error_message = unwords ["Incorrect hash computation of", key]- computed_hash = Hashing.hash 0 key---wordIndexTuples = [- ("apple", 1 :: Int)- , ("banana", 2)- , ("carrot", 3)- ]---intMapTuples :: [(Atom Int, Int)]-intMapTuples = [- (Atom 1000, 1)- , (Atom 5555, 2)- , (Atom 9876, 3)- ]---testHashLookups :: (Show (f a), Show b, Eq b, Vector.Unbox b, Construction.Defaultable b, Foldable f, Hashing.ToNumeric a, Eq (f a), Hashable (f a)) =>- [(f a, b)] -> IO ()-testHashLookups word_index_tuples =- assertBool "Perfect hash lookups failed to match the input" $ isRight test_result_either- where- lookup_table = Construction.createMinimalPerfectHash word_index_tuples- test_result_either = Exercise.testLookups lookup_table word_index_tuples---tests = [- testGroup "Hash computation" [- testCase "compute-hash1" $ testHashComputation "blarg" 3322346319- ]- , testGroup "Hash lookups" [- testCase "word-lookups" $ testHashLookups wordIndexTuples- , testCase "int-lookups" $ testHashLookups intMapTuples- ]- ]---main = defaultMain tests