perfect-hash-generator 0.1.0.4 → 0.2.0.0
raw patch · 10 files changed
+248/−208 lines, 10 filesdep +binarydep +bytestringdep +text
Dependencies added: binary, bytestring, text
Files
- demo-ints/Main.hs +0/−71
- demo-strings/Main.hs +0/−33
- demo/Ints/Main.hs +66/−0
- demo/Strings/Main.hs +33/−0
- perfect-hash-generator.cabal +49/−27
- src/Data/PerfectHash/Construction.hs +32/−26
- src/Data/PerfectHash/Hashing.hs +30/−16
- src/Data/PerfectHash/Lookup.hs +4/−3
- test/Exercise.hs +2/−16
- test/Main.hs +32/−16
− demo-ints/Main.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
@@ -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
+ demo/Ints/Main.hs view
@@ -0,0 +1,66 @@+module Main where++import System.Random (RandomGen, mkStdGen, random)++import Data.IntSet (IntSet)+import qualified Data.IntSet as IntSet+import qualified Data.PerfectHash.Construction as Construction+import qualified Data.PerfectHash.Lookup as Lookup+import qualified Data.Vector.Unboxed as Vector+import qualified Exercise+++valueCount = 250000+++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) = random 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 :: [(Int, Int)]+intMapTuples = zip 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
perfect-hash-generator.cabal view
@@ -1,9 +1,11 @@--- This file has been generated from package.yaml by hpack version 0.17.1.+-- This file has been generated from package.yaml by hpack version 0.20.0. -- -- see: https://github.com/sol/hpack+--+-- hash: fc4c141081795a861f9768551508e74ca60a61d1b8061163c7ee9567b112173c name: perfect-hash-generator-version: 0.1.0.4+version: 0.2.0.0 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@. .@@ -16,14 +18,13 @@ This implementation was adapted from <http://stevehanov.ca/blog/index.php?id=119 Steve Hanov's Blog>. . = Usage- The library is written generically to hash both strings and raw integers. Integers should be wrapped in the @Atom@ newtype:- .+ The library is written generically to hash both strings and raw integers according to the <http://isthe.com/chongo/tech/comp/fnv/ FNV-1a algorithm>. Integers are split by octets before hashing. > import Data.PerfectHash.Construction (createMinimalPerfectHash) > > tuples = [- > (Atom 1000, 1)- > , (Atom 5555, 2)- > , (Atom 9876, 3)+ > (1000, 1)+ > , (5555, 2)+ > , (9876, 3) > ] > > lookup_table = createMinimalPerfectHash tuples@@ -56,55 +57,72 @@ src ghc-options: -fwarn-tabs -W build-depends:- base >= 4.5 && <= 4.10- , unordered-containers+ base >=4.5 && <=4.10+ , binary+ , bytestring , containers , data-ordlist , directory , filepath , hashable+ , text+ , unordered-containers , vector exposed-modules: Data.PerfectHash.Construction Data.PerfectHash.Hashing Data.PerfectHash.Lookup+ other-modules:+ Paths_perfect_hash_generator default-language: Haskell2010 executable hash-perfectly-ints-demo- main-is: Main.hs+ main-is: Ints/Main.hs hs-source-dirs:- demo-ints+ demo test ghc-options: -fwarn-tabs -W build-depends:- base >= 4.5 && <= 4.10- , unordered-containers+ base >=4.5 && <=4.10+ , binary+ , bytestring+ , containers+ , hashable+ , optparse-applicative , perfect-hash-generator , random- , optparse-applicative+ , text+ , unordered-containers , vector- , hashable- , containers other-modules:+ Strings.Main Exercise+ Main+ Paths_perfect_hash_generator default-language: Haskell2010 executable hash-perfectly-strings-demo- main-is: Main.hs+ main-is: Strings/Main.hs hs-source-dirs:- demo-strings+ demo test ghc-options: -fwarn-tabs -W build-depends:- base >= 4.5 && <= 4.10- , unordered-containers+ base >=4.5 && <=4.10+ , binary+ , bytestring+ , hashable+ , optparse-applicative , perfect-hash-generator , random- , optparse-applicative+ , text+ , unordered-containers , vector- , hashable other-modules:+ Ints.Main Exercise+ Main+ Paths_perfect_hash_generator default-language: Haskell2010 test-suite regression-tests@@ -114,15 +132,19 @@ test ghc-options: -fwarn-tabs -W build-depends:- base >= 4.5 && <= 4.10- , unordered-containers- , perfect-hash-generator+ HUnit+ , base >=4.5 && <=4.10+ , binary+ , bytestring+ , hashable , optparse-applicative+ , perfect-hash-generator , test-framework- , HUnit , test-framework-hunit- , hashable+ , text+ , unordered-containers , vector other-modules: Exercise+ Paths_perfect_hash_generator default-language: Haskell2010
src/Data/PerfectHash/Construction.hs view
@@ -12,12 +12,14 @@ import Control.Arrow (second) import Control.Monad (join)+import Data.Foldable (foldl') import Data.Hashable (Hashable) import Data.HashMap.Strict (HashMap) import qualified Data.HashMap.Strict as HashMap import Data.IntSet (IntSet) import qualified Data.IntSet as IntSet import Data.List (sortOn)+import Data.Ord (Down (Down)) import qualified Data.Vector.Unboxed as Vector import qualified Data.PerfectHash.Hashing as Hashing@@ -32,13 +34,13 @@ } +emptyLookupTable :: LookupTable a emptyLookupTable = NewLookupTable HashMap.empty HashMap.empty class Defaultable a where getDefault :: a - instance Defaultable Int where getDefault = 0 @@ -58,14 +60,14 @@ -- 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) =>+attemptNonceRecursive :: Hashing.ToHashableChunks a => HashMapAndSize Int b- -> Int- -> IntSet- -> [f a]+ -> Int -- ^ nonce+ -> IntSet -- ^ occupied slots+ -> [a] -- ^ keys -> [Maybe Int] attemptNonceRecursive _ _ _ [] = []-attemptNonceRecursive values_and_size nonce occupied_slots (x:xs) =+attemptNonceRecursive values_and_size nonce occupied_slots (current_key:remaining_bucket_keys) = if cannot_use_slot then pure Nothing@@ -73,7 +75,7 @@ where HashMapAndSize values size = values_and_size- slot = Hashing.hashToSlot nonce x size+ slot = Hashing.hashToSlot nonce current_key size cannot_use_slot = IntSet.member slot occupied_slots || HashMap.member slot values @@ -81,7 +83,7 @@ values_and_size nonce (IntSet.insert slot occupied_slots)- xs+ remaining_bucket_keys -- | Repeatedly try different values of the nonce until we find a hash function@@ -89,10 +91,10 @@ -- -- 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+findNonceForBucket :: Hashing.ToHashableChunks a =>+ Int -- ^ nonce to attempt -> HashMapAndSize Int b- -> [f a]+ -> [a] -- ^ colliding keys for this bucket -> ([Int], Int) findNonceForBucket nonce_attempt values_and_size bucket = @@ -109,12 +111,12 @@ -- | 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])+handleMultiBuckets :: (Hashing.ToHashableChunks a, Eq a, Hashable a) =>+ HashMapAndSize a b -> LookupTable b+ -> (Int, [a]) -> LookupTable b-handleMultiBuckets sized_words_dict (computed_hash, bucket) old_lookup_table =+handleMultiBuckets sized_words_dict old_lookup_table (computed_hash, bucket) = NewLookupTable new_g new_values where HashMapAndSize words_dict size = sized_words_dict@@ -132,10 +134,10 @@ -- | This function exploits the sorted structure of the list twice, -- first by skimming the multi-entry buckets, then by skimming -- the single-entry buckets and dropping the empty buckets.-findCollisionNonces :: (Foldable f, Hashing.ToNumeric a, Eq (f a), Hashable (f a)) =>- HashMapAndSize (f a) b- -> [(Int, [f a])]- -> (LookupTable b, [(Int, f a)])+findCollisionNonces :: (Hashing.ToHashableChunks a, Eq a, Hashable a) =>+ HashMapAndSize a b+ -> [(Int, [a])]+ -> (LookupTable b, [(Int, a)]) findCollisionNonces sized_words_dict sorted_bucket_hash_tuples = (lookup_table, remaining_words)@@ -146,18 +148,22 @@ -- we know there are no more collision buckets. (multi_entry_buckets, single_or_fewer_buckets) = span ((> 1) . length . snd) sorted_bucket_hash_tuples - lookup_table = foldr (handleMultiBuckets sized_words_dict) emptyLookupTable multi_entry_buckets+ -- XXX Using 'foldl' rather than 'foldr' is crucial here, given the order+ -- of the buckets. 'foldr' would actually try to place the smallest buckets+ -- first, making it improbable that the large buckets will be placeable,+ -- and potentially resulting in an infinite loop.+ lookup_table = foldl' (handleMultiBuckets sized_words_dict) emptyLookupTable multi_entry_buckets single_entry_buckets = takeWhile (not . null . snd) single_or_fewer_buckets remaining_words = map (second head) single_entry_buckets -- | Sort buckets by descending size-preliminaryBucketPlacement :: (Foldable f, Hashing.ToNumeric a, Eq (f a), Hashable (f a)) =>- HashMap (f a) b- -> [(Int, [f a])]+preliminaryBucketPlacement :: (Hashing.ToHashableChunks a, Eq a, Hashable a) =>+ HashMap a b+ -> [(Int, [a])] preliminaryBucketPlacement words_dict =- sortOn (negate . length . snd) bucket_hash_tuples+ sortOn (Down . length . snd) bucket_hash_tuples where size = HashMap.size words_dict slot_key_pairs = deriveTuples (\k -> Hashing.hashToSlot 0 k size) $ HashMap.keys words_dict@@ -171,8 +177,8 @@ -- The values may be of arbitrary type. -- -- /__N.b.__/ It is assumed that the input tuples list has no duplicate keys.-createMinimalPerfectHash :: (Vector.Unbox b, Defaultable b, Foldable f, Hashing.ToNumeric a, Eq (f a), Hashable (f a)) =>- [(f a, b)]+createMinimalPerfectHash :: (Vector.Unbox b, Defaultable b, Hashing.ToHashableChunks a, Eq a, Hashable a) =>+ [(a, b)] -- ^ key-value pairs -> Lookup.LookupTable b createMinimalPerfectHash tuples = convertToVector $ NewLookupTable final_g final_values
src/Data/PerfectHash/Hashing.hs view
@@ -1,37 +1,47 @@ {-# OPTIONS_HADDOCK prune #-} +{-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE TypeSynonymInstances #-}++ -- | Implements the specialized hash function for -- this perfect hashing algorithm. module Data.PerfectHash.Hashing where -import Data.Bits (xor, (.&.))-import Data.Char (ord)+import Data.Binary (encode)+import Data.Bits (xor, (.&.))+import Data.ByteString.Lazy (unpack)+import Data.Char (ord)+import Data.Text (Text)+import qualified Data.Text as T -- | This choice of prime number was taken from the Python implementation -- on <http://stevehanov.ca/blog/index.php?id=119 Steve Hanov's page>.+primeFNV :: Int primeFNV = 0x01000193 +mask32bits :: Int mask32bits = 0xffffffff --- | A Foldable of any data type may be hashed, so long as it implements--- an instance of this class.-class ToNumeric a where- toNum :: a -> Int+class ToHashableChunks a where+ toHashableChunks :: a -> [Int] --- | The numeric value of a character is simply its ordinal value.-instance ToNumeric Char where- toNum = ord+instance ToHashableChunks Int where+ toHashableChunks = map fromIntegral . unpack . encode -instance ToNumeric Int where- toNum = id+instance ToHashableChunks Text where+ toHashableChunks = map ord . T.unpack +instance ToHashableChunks String where+ toHashableChunks = map ord -hashToSlot :: (Foldable f, ToNumeric a) =>++hashToSlot :: ToHashableChunks a => Int -- ^ nonce- -> f a -- ^ key+ -> a -- ^ key -> Int -- ^ array size -> Int hashToSlot nonce key size = hash nonce key `mod` size@@ -49,13 +59,17 @@ -- The interface is comparable to the -- <https://hackage.haskell.org/package/hashable-1.2.6.1/docs/Data-Hashable.html#v:hashWithSalt hashWithSalt> -- function from the @hashable@ package.-hash :: (Foldable f, ToNumeric a) => Int -> f a -> Int+hash :: ToHashableChunks a =>+ Int -- ^ nonce+ -> a -- ^ key+ -> Int hash nonce = - foldl combine d -- NOTE: This must be 'foldl', not 'foldr'+ -- NOTE: This must be 'foldl', not 'foldr'+ foldl combine d . toHashableChunks where d = if nonce == 0 then primeFNV else nonce - combine acc = (.&. mask32bits) . (* primeFNV) . xor acc . toNum+ combine acc = (.&. mask32bits) . (* primeFNV) . xor acc
src/Data/PerfectHash/Lookup.hs view
@@ -1,7 +1,7 @@ {-# OPTIONS_HADDOCK prune #-} -- | Note that what is referred to as a \"nonce\" in this library may be--- referred to by some as a \"salt\".+-- equivalently described as a \"salt\" by some. module Data.PerfectHash.Lookup ( LookupTable (LookupTable) , nonces@@ -46,6 +46,7 @@ size = Vector.length . values +encodeDirectEntry :: Int -> Int encodeDirectEntry = subtract 1 . negate @@ -68,9 +69,9 @@ -- respect to the length of the 'values' array. -- -- 3. Use the result of (2) as the index into the 'values' array.-lookupPerfect :: (Foldable f, Hashing.ToNumeric a, Vector.Unbox b) =>+lookupPerfect :: (Hashing.ToHashableChunks a, Vector.Unbox b) => LookupTable b- -> f a+ -> a -- ^ key -> b lookupPerfect lookup_table key =
test/Exercise.hs view
@@ -1,30 +1,16 @@-{-# LANGUAGE DeriveGeneric #-}- module Exercise where import Control.Monad (unless) import Data.Foldable (traverse_)-import Data.Hashable (Hashable) import qualified Data.Vector.Unboxed as Vector-import GHC.Generics (Generic) import qualified Data.PerfectHash.Hashing as Hashing import qualified Data.PerfectHash.Lookup as Lookup --- | Wrapper to allow hashing of an integer-newtype Atom a = Atom {value :: a} deriving (Eq, Show, Generic)--instance Hashable a => Hashable (Atom a)---instance Foldable Atom where- foldr f acc (Atom val) = f val acc---testLookups :: (Show b, Eq b, Show (f a), Foldable f, Hashing.ToNumeric a, Vector.Unbox b) =>+testLookups :: (Show b, Eq b, Show a, Hashing.ToHashableChunks a, Vector.Unbox b) => Lookup.LookupTable b- -> [(f a, b)]+ -> [(a, b)] -> Either String () testLookups lookup_table = traverse_ check_entry
test/Main.hs view
@@ -1,7 +1,10 @@+{-# LANGUAGE OverloadedStrings #-}+ module Main where import Data.Either (isRight) import Data.Hashable (Hashable)+import Data.Text (Text) import qualified Data.Vector.Unboxed as Vector import Test.Framework (defaultMain, testGroup) import Test.Framework.Providers.HUnit (testCase)@@ -9,35 +12,46 @@ 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 :: (Hashing.ToHashableChunks a, Show a) =>+ a+ -> Int+ -> IO () testHashComputation key val = assertEqual error_message val computed_hash where- error_message = unwords ["Incorrect hash computation of", key]+ error_message = unwords ["Incorrect hash computation of", show key] computed_hash = Hashing.hash 0 key -wordIndexTuples = [- ("apple", 1 :: Int)- , ("banana", 2)- , ("carrot", 3)- ]+wordIndexTuplesString :: [(String, Int)]+wordIndexTuplesString = zip [+ "apple"+ , "banana"+ , "carrot"+ ] [1..] -intMapTuples :: [(Atom Int, Int)]+wordIndexTuplesText :: [(Text, Int)]+wordIndexTuplesText = zip [+ "alpha"+ , "beta"+ , "gamma"+ ] [1..]+++intMapTuples :: [(Int, Int)] intMapTuples = [- (Atom 1000, 1)- , (Atom 5555, 2)- , (Atom 9876, 3)+ (1000, 1)+ , (5555, 2)+ , (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 :: (Show a, Show b, Eq b, Vector.Unbox b, Construction.Defaultable b, Hashing.ToHashableChunks a, Eq a, Hashable a) =>+ [(a, b)] -> IO () testHashLookups word_index_tuples = assertBool "Perfect hash lookups failed to match the input" $ isRight test_result_either where@@ -47,10 +61,12 @@ tests = [ testGroup "Hash computation" [- testCase "compute-hash1" $ testHashComputation "blarg" 3322346319+ testCase "compute-string-hash" $ testHashComputation ("blarg" :: String) 3322346319+ , testCase "compute-int-hash" $ testHashComputation (70000 :: Int) 4169891409 ] , testGroup "Hash lookups" [- testCase "word-lookups" $ testHashLookups wordIndexTuples+ testCase "word-lookups-string" $ testHashLookups wordIndexTuplesString+ , testCase "word-lookups-text" $ testHashLookups wordIndexTuplesText , testCase "int-lookups" $ testHashLookups intMapTuples ] ]