packages feed

perfect-hash-generator 0.2.0.6 → 1.0.0

raw patch · 11 files changed

+658/−247 lines, 11 filesdep +data-defaultdep +sorted-listPVP ok

version bump matches the API change (PVP)

Dependencies added: data-default, sorted-list

API changes (from Hackage documentation)

- Data.PerfectHash.Construction: class Defaultable a
- Data.PerfectHash.Construction: instance Data.PerfectHash.Construction.Defaultable GHC.Types.Int
+ Data.PerfectHash.Construction: instance Data.Default.Class.Default Data.PerfectHash.Construction.NonceOrDirect
+ Data.PerfectHash.Construction: instance GHC.Classes.Eq (Data.PerfectHash.Construction.HashBucket a)
+ Data.PerfectHash.Construction: instance GHC.Classes.Eq a => GHC.Classes.Eq (Data.PerfectHash.Construction.SingletonBucket a)
+ Data.PerfectHash.Construction: instance GHC.Classes.Ord (Data.PerfectHash.Construction.HashBucket a)
+ Data.PerfectHash.Hashing: instance GHC.Classes.Eq Data.PerfectHash.Hashing.Hash
+ Data.PerfectHash.Hashing: instance GHC.Show.Show Data.PerfectHash.Hashing.ArraySize
+ Data.PerfectHash.Hashing: instance GHC.Show.Show Data.PerfectHash.Hashing.Hash
- Data.PerfectHash.Construction: createMinimalPerfectHash :: (Unbox b, Defaultable b, ToHashableChunks a, Eq a, Hashable a) => HashMap a b -> LookupTable b
+ Data.PerfectHash.Construction: createMinimalPerfectHash :: (ToHashableChunks k, Default v) => Map k v -> LookupTable v
- Data.PerfectHash.Hashing: hash :: ToHashableChunks a => Int -> a -> Int
+ Data.PerfectHash.Hashing: hash :: ToHashableChunks a => Nonce -> a -> Hash
- Data.PerfectHash.Hashing: toHashableChunks :: ToHashableChunks a => a -> [Int]
+ Data.PerfectHash.Hashing: toHashableChunks :: ToHashableChunks a => a -> [Hash]
- Data.PerfectHash.Lookup: lookup :: (ToHashableChunks a, Unbox b) => LookupTable b -> a -> b
+ Data.PerfectHash.Lookup: lookup :: ToHashableChunks a => LookupTable b -> a -> b

Files

+ changelog.md view
@@ -0,0 +1,9 @@+## 0.2.0.1 (Feb. 2018)++* Fixed a foldr vs. foldl bug with algorithmic implications++## 1.0.0 (June 2022)++* Changed input type from `HashMap` to `Map`+* Removed superfluous internal map lookups by threading values alongside keys throughout the algorithm+* Used newtypes internally for algorithmic clarity
demo/ints/Main.hs view
@@ -1,53 +1,60 @@ module Main where -import           System.Random                 (RandomGen, mkStdGen, random)+import qualified Data.Vector           as Vector+import           System.CPUTime+import           Text.Printf+import Options.Applicative -import           Data.HashMap.Strict           (HashMap)-import qualified Data.HashMap.Strict           as HashMap-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 Data.PerfectHash.Types.Nonces as Nonces+ import qualified Exercise  -valueCount = 250000+defaultValueCount :: Int+defaultValueCount = 250000  -data RandIntAccum t = RandIntAccum-  t -- ^ random number generator-  Int -- ^ max count-  IntSet -- ^ accumulated unique random numbers+data DemoOptions = DemoOptions {+    valueCount :: Int+  , debugEnabled :: Bool+  }  --- | 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) =+optionsParser :: Parser DemoOptions+optionsParser = DemoOptions+  <$> option auto+      ( long "count"+      <> help "Value count"+      <> value defaultValueCount)+  <*> switch+      ( long "debug"+      <> help "enable debug mode") -  if count == 0-    then current_set-    else getUniqueRandomIntegers newstate +main :: IO ()+main = run =<< execParser opts   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)+    opts = info (optionsParser <**> helper)+      ( fullDesc+     <> progDesc "Test the hashing on integers"+     <> header "int test" )  -intMapTuples :: HashMap Int Int-intMapTuples = HashMap.fromList $ zip random_ints [1..]-  where-    seed_value = RandIntAccum (mkStdGen 0) valueCount IntSet.empty-    random_ints = IntSet.toList $ getUniqueRandomIntegers seed_value+doTimed :: Either String a -> IO Double+doTimed go = do+  start <- getCPUTime+  Exercise.eitherExit go+  end   <- getCPUTime+  return $ fromIntegral (end - start) / (10^12)  -main = do-  putStrLn $ unwords ["Keys size:", show $ length intMapTuples]+run (DemoOptions valueCount _debugEnabled) = do+  putStrLn $ unwords [+      "Keys size:"+    , show $ length intMapTuples+    ]    let lookup_table = Construction.createMinimalPerfectHash intMapTuples @@ -57,12 +64,27 @@     , "entries."     ] -  let direct_mapping_nonces = Vector.filter (< 0) $ Lookup.nonces lookup_table+  let direct_mapping_nonces = Vector.filter Nonces.isDirectSlot $ Lookup.nonces lookup_table+      direct_mapping_count = Vector.length direct_mapping_nonces+      total_count = length intMapTuples+      direct_mapping_percentage = 100 * direct_mapping_count `div` total_count    putStrLn $ unwords [       "There were"     , show $ Vector.length direct_mapping_nonces+    , "(" ++ show direct_mapping_percentage ++ "%)"     , "lookup entries with direct mappings."     ] -  Exercise.eitherExit $ Exercise.testLookups lookup_table intMapTuples+  putStrLn "Testing perfect hash lookups..."+  diff1 <- doTimed $ Exercise.testPerfectLookups lookup_table intMapTuples+  putStrLn $ printf "Computation time: %0.3f sec\n" diff1++  putStrLn "Testing HashMap lookups..."+  diff2 <- doTimed $ Exercise.testHashMapLookups intMapTuples+  putStrLn $ printf "Computation time: %0.3f sec\n" diff2++  putStrLn "Done."++  where+    intMapTuples = Exercise.mkIntMapTuples valueCount
demo/strings/Main.hs view
@@ -1,25 +1,54 @@ module Main where  import           Control.Monad                 (when)-import qualified Data.HashMap.Strict           as HashMap-+import qualified Data.Map as Map+import Options.Applicative import qualified Data.PerfectHash.Construction as Construction import qualified Data.PerfectHash.Lookup       as Lookup import qualified Exercise  -enableDebug = False+defaultDictionaryPath :: FilePath+defaultDictionaryPath = "/usr/share/dict/words" -dictionaryPath = "/usr/share/dict/words" +data DemoOptions = DemoOptions {+    dictionaryPath :: FilePath+  , debugEnabled :: Bool+  } -main = do +optionsParser :: Parser DemoOptions+optionsParser = DemoOptions+  <$> strOption+      ( long "dictionary"+      <> help "Dictionary path"+      <> value defaultDictionaryPath)+  <*> switch+      ( long "debug"+      <> help "enable debug mode")+++main :: IO ()+main = run =<< execParser opts+  where+    opts = info (optionsParser <**> helper)+      ( fullDesc+     <> progDesc "Test the hashing on strings"+     <> header "string test" )+++run (DemoOptions dictionaryPath enableDebug) = do+   word_index_tuples <- Exercise.wordsFromFile dictionaryPath -  putStrLn $ unwords ["Words size:", show $ length word_index_tuples]+  putStrLn $ unwords [+      "Words size:"+    , show $ length word_index_tuples+    ] -  let lookup_table = Construction.createMinimalPerfectHash $ HashMap.fromList word_index_tuples+  let lookup_table = Construction.createMinimalPerfectHash $+        Map.fromList word_index_tuples    putStrLn $ unwords [       "Finished computing lookup table with"@@ -31,4 +60,5 @@     putStrLn $ unwords ["Vector G:", show $ Lookup.nonces lookup_table]     putStrLn $ unwords ["Vector V:", show $ Lookup.values lookup_table] -  Exercise.eitherExit $ Exercise.testLookups lookup_table $ HashMap.fromList word_index_tuples+  Exercise.eitherExit $ Exercise.testPerfectLookups lookup_table $+    Map.fromList word_index_tuples
+ docs/images/algorithm-diagram.svg view
@@ -0,0 +1,3 @@+<?xml version="1.0" encoding="UTF-8"?>+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"+    "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg stroke="rgb(0,0,0)" xmlns="http://www.w3.org/2000/svg" font-size="1" width="260.0000" stroke-opacity="1" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0.0 0.0 260.00000000000006 220.0" height="220.0000" version="1.1"><defs></defs><g fill="rgb(0,0,0)" stroke="rgb(0,0,0)" stroke-linecap="butt" stroke-width="0.956660859448112" font-size="15.0px" stroke-miterlimit="10.0" fill-opacity="1.0" stroke-opacity="1.0" stroke-linejoin="miter"><text dominant-baseline="middle" stroke="none" transform="matrix(1.0000,0.0000,0.0000,1.0000,200.0000,205.0000)" text-anchor="middle">value table</text></g><g fill="rgb(222,203,228)" stroke="rgb(0,0,0)" stroke-linecap="butt" stroke-width="0.0" stroke-miterlimit="10.0" fill-opacity="1.0" stroke-opacity="1.0" stroke-linejoin="miter"><path d="M 220.0000,185.0000 v -20.0000 c 0.0000,-2.7614 -2.2386,-5.0000 -5.0000 -5.0000h -30.0000 c -2.7614,-0.0000 -5.0000,2.2386 -5.0000 5.0000v 20.0000 c -0.0000,2.7614 2.2386,5.0000 5.0000 5.0000h 30.0000 c 2.7614,0.0000 5.0000,-2.2386 5.0000 -5.0000Z"/></g><g fill="rgb(0,0,0)" stroke="rgb(0,0,0)" stroke-linecap="butt" stroke-width="0.956660859448112" font-size="20.0px" stroke-miterlimit="10.0" fill-opacity="1.0" stroke-opacity="1.0" stroke-linejoin="miter"><text dominant-baseline="middle" stroke="none" transform="matrix(1.0000,0.0000,0.0000,1.0000,200.0000,175.0000)" text-anchor="middle">3</text></g><g fill="rgb(222,203,228)" stroke="rgb(0,0,0)" stroke-linecap="butt" stroke-width="0.0" stroke-miterlimit="10.0" fill-opacity="1.0" stroke-opacity="1.0" stroke-linejoin="miter"><path d="M 220.0000,145.0000 v -20.0000 c 0.0000,-2.7614 -2.2386,-5.0000 -5.0000 -5.0000h -30.0000 c -2.7614,-0.0000 -5.0000,2.2386 -5.0000 5.0000v 20.0000 c -0.0000,2.7614 2.2386,5.0000 5.0000 5.0000h 30.0000 c 2.7614,0.0000 5.0000,-2.2386 5.0000 -5.0000Z"/></g><g fill="rgb(0,0,0)" stroke="rgb(0,0,0)" stroke-linecap="butt" stroke-width="0.956660859448112" font-size="20.0px" stroke-miterlimit="10.0" fill-opacity="1.0" stroke-opacity="1.0" stroke-linejoin="miter"><text dominant-baseline="middle" stroke="none" transform="matrix(1.0000,0.0000,0.0000,1.0000,200.0000,135.0000)" text-anchor="middle">3</text></g><g fill="rgb(222,203,228)" stroke="rgb(0,0,0)" stroke-linecap="butt" stroke-width="0.0" stroke-miterlimit="10.0" fill-opacity="1.0" stroke-opacity="1.0" stroke-linejoin="miter"><path d="M 220.0000,105.0000 v -20.0000 c 0.0000,-2.7614 -2.2386,-5.0000 -5.0000 -5.0000h -30.0000 c -2.7614,-0.0000 -5.0000,2.2386 -5.0000 5.0000v 20.0000 c -0.0000,2.7614 2.2386,5.0000 5.0000 5.0000h 30.0000 c 2.7614,0.0000 5.0000,-2.2386 5.0000 -5.0000Z"/></g><g fill="rgb(0,0,0)" stroke="rgb(0,0,0)" stroke-linecap="butt" stroke-width="0.956660859448112" font-size="20.0px" stroke-miterlimit="10.0" fill-opacity="1.0" stroke-opacity="1.0" stroke-linejoin="miter"><text dominant-baseline="middle" stroke="none" transform="matrix(1.0000,0.0000,0.0000,1.0000,200.0000,95.0000)" text-anchor="middle">3</text></g><g fill="rgb(222,203,228)" stroke="rgb(0,0,0)" stroke-linecap="butt" stroke-width="0.0" stroke-miterlimit="10.0" fill-opacity="1.0" stroke-opacity="1.0" stroke-linejoin="miter"><path d="M 220.0000,65.0000 v -20.0000 c 0.0000,-2.7614 -2.2386,-5.0000 -5.0000 -5.0000h -30.0000 c -2.7614,-0.0000 -5.0000,2.2386 -5.0000 5.0000v 20.0000 c -0.0000,2.7614 2.2386,5.0000 5.0000 5.0000h 30.0000 c 2.7614,0.0000 5.0000,-2.2386 5.0000 -5.0000Z"/></g><g fill="rgb(0,0,0)" stroke="rgb(0,0,0)" stroke-linecap="butt" stroke-width="0.956660859448112" font-size="20.0px" stroke-miterlimit="10.0" fill-opacity="1.0" stroke-opacity="1.0" stroke-linejoin="miter"><text dominant-baseline="middle" stroke="none" transform="matrix(1.0000,0.0000,0.0000,1.0000,200.0000,55.0000)" text-anchor="middle">3</text></g><g fill="rgb(222,203,228)" stroke="rgb(0,0,0)" stroke-linecap="butt" stroke-width="0.0" stroke-miterlimit="10.0" fill-opacity="1.0" stroke-opacity="1.0" stroke-linejoin="miter"><path d="M 220.0000,25.0000 v -20.0000 c 0.0000,-2.7614 -2.2386,-5.0000 -5.0000 -5.0000h -30.0000 c -2.7614,-0.0000 -5.0000,2.2386 -5.0000 5.0000v 20.0000 c -0.0000,2.7614 2.2386,5.0000 5.0000 5.0000h 30.0000 c 2.7614,0.0000 5.0000,-2.2386 5.0000 -5.0000Z"/></g><g fill="rgb(0,0,0)" stroke="rgb(0,0,0)" stroke-linecap="butt" stroke-width="0.956660859448112" font-size="20.0px" stroke-miterlimit="10.0" fill-opacity="1.0" stroke-opacity="1.0" stroke-linejoin="miter"><text dominant-baseline="middle" stroke="none" transform="matrix(1.0000,0.0000,0.0000,1.0000,200.0000,15.0000)" text-anchor="middle">3</text></g><g fill="rgb(0,0,0)" stroke="rgb(0,0,0)" stroke-linecap="butt" stroke-width="0.956660859448112" font-size="15.0px" stroke-miterlimit="10.0" fill-opacity="1.0" stroke-opacity="1.0" stroke-linejoin="miter"><text dominant-baseline="middle" stroke="none" transform="matrix(1.0000,0.0000,0.0000,1.0000,60.0000,205.0000)" text-anchor="middle">intermediate table</text></g><g fill="rgb(254,217,166)" stroke="rgb(0,0,0)" stroke-linecap="butt" stroke-width="0.0" stroke-miterlimit="10.0" fill-opacity="1.0" stroke-opacity="1.0" stroke-linejoin="miter"><path d="M 80.0000,185.0000 v -20.0000 c 0.0000,-2.7614 -2.2386,-5.0000 -5.0000 -5.0000h -30.0000 c -2.7614,-0.0000 -5.0000,2.2386 -5.0000 5.0000v 20.0000 c -0.0000,2.7614 2.2386,5.0000 5.0000 5.0000h 30.0000 c 2.7614,0.0000 5.0000,-2.2386 5.0000 -5.0000Z"/></g><g fill="rgb(0,0,0)" stroke="rgb(0,0,0)" stroke-linecap="butt" stroke-width="0.956660859448112" font-size="20.0px" stroke-miterlimit="10.0" fill-opacity="1.0" stroke-opacity="1.0" stroke-linejoin="miter"><text dominant-baseline="middle" stroke="none" transform="matrix(1.0000,0.0000,0.0000,1.0000,60.0000,175.0000)" text-anchor="middle"></text></g><g fill="rgb(254,217,166)" stroke="rgb(0,0,0)" stroke-linecap="butt" stroke-width="0.0" stroke-miterlimit="10.0" fill-opacity="1.0" stroke-opacity="1.0" stroke-linejoin="miter"><path d="M 80.0000,145.0000 v -20.0000 c 0.0000,-2.7614 -2.2386,-5.0000 -5.0000 -5.0000h -30.0000 c -2.7614,-0.0000 -5.0000,2.2386 -5.0000 5.0000v 20.0000 c -0.0000,2.7614 2.2386,5.0000 5.0000 5.0000h 30.0000 c 2.7614,0.0000 5.0000,-2.2386 5.0000 -5.0000Z"/></g><g fill="rgb(0,0,0)" stroke="rgb(0,0,0)" stroke-linecap="butt" stroke-width="0.956660859448112" font-size="20.0px" stroke-miterlimit="10.0" fill-opacity="1.0" stroke-opacity="1.0" stroke-linejoin="miter"><text dominant-baseline="middle" stroke="none" transform="matrix(1.0000,0.0000,0.0000,1.0000,60.0000,135.0000)" text-anchor="middle">blah</text></g><g fill="rgb(254,217,166)" stroke="rgb(0,0,0)" stroke-linecap="butt" stroke-width="0.0" stroke-miterlimit="10.0" fill-opacity="1.0" stroke-opacity="1.0" stroke-linejoin="miter"><path d="M 80.0000,105.0000 v -20.0000 c 0.0000,-2.7614 -2.2386,-5.0000 -5.0000 -5.0000h -30.0000 c -2.7614,-0.0000 -5.0000,2.2386 -5.0000 5.0000v 20.0000 c -0.0000,2.7614 2.2386,5.0000 5.0000 5.0000h 30.0000 c 2.7614,0.0000 5.0000,-2.2386 5.0000 -5.0000Z"/></g><g fill="rgb(0,0,0)" stroke="rgb(0,0,0)" stroke-linecap="butt" stroke-width="0.956660859448112" font-size="20.0px" stroke-miterlimit="10.0" fill-opacity="1.0" stroke-opacity="1.0" stroke-linejoin="miter"><text dominant-baseline="middle" stroke="none" transform="matrix(1.0000,0.0000,0.0000,1.0000,60.0000,95.0000)" text-anchor="middle"></text></g><g fill="rgb(254,217,166)" stroke="rgb(0,0,0)" stroke-linecap="butt" stroke-width="0.0" stroke-miterlimit="10.0" fill-opacity="1.0" stroke-opacity="1.0" stroke-linejoin="miter"><path d="M 80.0000,65.0000 v -20.0000 c 0.0000,-2.7614 -2.2386,-5.0000 -5.0000 -5.0000h -30.0000 c -2.7614,-0.0000 -5.0000,2.2386 -5.0000 5.0000v 20.0000 c -0.0000,2.7614 2.2386,5.0000 5.0000 5.0000h 30.0000 c 2.7614,0.0000 5.0000,-2.2386 5.0000 -5.0000Z"/></g><g fill="rgb(0,0,0)" stroke="rgb(0,0,0)" stroke-linecap="butt" stroke-width="0.956660859448112" font-size="20.0px" stroke-miterlimit="10.0" fill-opacity="1.0" stroke-opacity="1.0" stroke-linejoin="miter"><text dominant-baseline="middle" stroke="none" transform="matrix(1.0000,0.0000,0.0000,1.0000,60.0000,55.0000)" text-anchor="middle">bar</text></g><g fill="rgb(254,217,166)" stroke="rgb(0,0,0)" stroke-linecap="butt" stroke-width="0.0" stroke-miterlimit="10.0" fill-opacity="1.0" stroke-opacity="1.0" stroke-linejoin="miter"><path d="M 80.0000,25.0000 v -20.0000 c 0.0000,-2.7614 -2.2386,-5.0000 -5.0000 -5.0000h -30.0000 c -2.7614,-0.0000 -5.0000,2.2386 -5.0000 5.0000v 20.0000 c -0.0000,2.7614 2.2386,5.0000 5.0000 5.0000h 30.0000 c 2.7614,0.0000 5.0000,-2.2386 5.0000 -5.0000Z"/></g><g fill="rgb(0,0,0)" stroke="rgb(0,0,0)" stroke-linecap="butt" stroke-width="0.956660859448112" font-size="20.0px" stroke-miterlimit="10.0" fill-opacity="1.0" stroke-opacity="1.0" stroke-linejoin="miter"><text dominant-baseline="middle" stroke="none" transform="matrix(1.0000,0.0000,0.0000,1.0000,60.0000,15.0000)" text-anchor="middle">foo</text></g></svg>
perfect-hash-generator.cabal view
@@ -1,11 +1,11 @@--- This file has been generated from package.yaml by hpack version 0.20.0.+cabal-version: 1.18++-- This file has been generated from package.yaml by hpack version 0.34.4. -- -- see: https://github.com/sol/hpack------ hash: 5294c49846f25b6cb06c72295e7bffbdf2443d5e99a7f79652dd5176510b48b5  name:           perfect-hash-generator-version:        0.2.0.6+version:        1.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@.                 .@@ -13,7 +13,7 @@                 .                 It is intended primarily for generating C code for embedded applications (compare to @<https://www.gnu.org/software/gperf/manual/gperf.html#Search-Structures gperf>@). The output of this tool is a pair of arrays that can be included in generated C code for __<https://en.wikipedia.org/wiki/C_dynamic_memory_allocation allocation-free> hash tables__.                 .-                Though lookups also perform reasonably well for Haskell applications, it hasn't been benchmarked thorougly with respect to other data structures.+                Though conceivably this data structure could be used directly in Haskell applications as a read-only hash table, it is not recommened, as lookups are about 10x slower than <https://hackage.haskell.org/package/unordered-containers/docs/Data-HashMap-Strict.html#t:HashMap HashMap>.                 .                 This implementation was adapted from <http://stevehanov.ca/blog/index.php?id=119 Steve Hanov's Blog>.                 .@@ -21,7 +21,7 @@                 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)-                > import qualified Data.HashMap.Strict as HashMap+                > import qualified Data.Map as Map                 >                 > tuples = [                 >    (1000, 1)@@ -29,10 +29,11 @@                 >  , (9876, 3)                 >  ]                 >-                > lookup_table = createMinimalPerfectHash $ HashMap.fromList tuples+                > lookup_table = createMinimalPerfectHash $ Map.fromList tuples                 .                 Generation of C code based on the arrays in @lookup_table@ is left as an exercise to the reader. Algorithm documentation in the "Data.PerfectHash.Hashing" and "Data.PerfectHash.Lookup" modules will be helpful.                 .+                = Demo                 See the @hash-perfectly-strings-demo@ and @hash-perfectly-ints-demo@, as well as the test suite, for working examples.                 .                 > $ stack build@@ -45,13 +46,23 @@ license:        Apache-2.0 license-file:   LICENSE build-type:     Simple-cabal-version:  >= 1.10+extra-source-files:+    changelog.md+extra-doc-files:+    docs/images/algorithm-diagram.svg  source-repository head   type: git   location: https://github.com/kostmo/perfect-hash-generator  library+  exposed-modules:+      Data.PerfectHash.Construction+      Data.PerfectHash.Hashing+      Data.PerfectHash.Lookup+      Data.PerfectHash.Types.Nonces+  other-modules:+      Paths_perfect_hash_generator   hs-source-dirs:       src   ghc-options: -fwarn-tabs -W@@ -60,23 +71,22 @@     , binary     , bytestring     , containers+    , data-default     , data-ordlist     , directory     , filepath     , hashable+    , sorted-list     , 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+  other-modules:+      Exercise+      Paths_perfect_hash_generator   hs-source-dirs:       demo/ints       test-utils@@ -93,13 +103,13 @@     , text     , unordered-containers     , vector-  other-modules:-      Exercise-      Paths_perfect_hash_generator   default-language: Haskell2010  executable hash-perfectly-strings-demo   main-is: Main.hs+  other-modules:+      Exercise+      Paths_perfect_hash_generator   hs-source-dirs:       demo/strings       test-utils@@ -108,6 +118,7 @@       base >=4.5 && <5     , binary     , bytestring+    , containers     , hashable     , optparse-applicative     , perfect-hash-generator@@ -115,14 +126,14 @@     , text     , unordered-containers     , vector-  other-modules:-      Exercise-      Paths_perfect_hash_generator   default-language: Haskell2010  test-suite regression-tests   type: exitcode-stdio-1.0   main-is: Main.hs+  other-modules:+      Exercise+      Paths_perfect_hash_generator   hs-source-dirs:       test       test-utils@@ -132,15 +143,15 @@     , base >=4.5 && <5     , binary     , bytestring+    , containers+    , data-default     , hashable     , optparse-applicative     , perfect-hash-generator+    , random     , test-framework     , test-framework-hunit     , text     , unordered-containers     , vector-  other-modules:-      Exercise-      Paths_perfect_hash_generator   default-language: Haskell2010
src/Data/PerfectHash/Construction.hs view
@@ -2,229 +2,416 @@  -- | Constructs a minimal perfect hash from a map of key-value pairs. ----- Implementation was adapted from--- <http://stevehanov.ca/blog/index.php?id=119 Steve Hanov's Blog>.+-- = Overview of algorithm+-- A two-input hash function @F(nonce, key)@ is used. --+-- 1. Keys are hashed into buckets for the first round with a nonce of @0@.+-- 1. Iterating over each bucket of size @>= 2@ in order of decreasing size, keep+--    testing different nonce values until all members+--    of the bucket fall into open slots in the final array.+--    When a successful nonce is found, write it to the \"intermediate\" array+--    at the bucket's position.+-- 1. For each bucket of size @1@, select an arbitrary open slot in the final+--    array, and write the slot's+--    index (after negation and subtracting @1@) to the intermediate array.+--+-- According to <http://cmph.sourceforge.net/papers/esa09.pdf this paper>,+-- the algorithm is assured to run in linear time.+--+-- = Provenance+-- This implementation was adapted from+-- <http://stevehanov.ca/blog/index.php?id=119 Steve Hanov's Blog>. -- A refactoring of that Python implementation may be found -- <https://github.com/kostmo/perfect-hash-generator/blob/master/python/perfect-hash.py here>.--- This Haskell implementation is transliterated from that refactoring.+-- This Haskell implementation was transliterated and evolved from that refactoring.+-- module Data.PerfectHash.Construction (     createMinimalPerfectHash-  , Defaultable   ) where -import           Control.Arrow            (second)+import Control.Arrow (first)+import Data.Tuple (swap)+import           Data.Default             (Default, def) import           Control.Monad            (join)+import Data.SortedList (SortedList, toSortedList, fromSortedList) 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.IntSet              (IntSet)+import qualified Data.IntMap              as IntMap+import           Data.IntMap              (IntMap)+import qualified Data.Map as Map+import           Data.Map                 (Map)+import Data.Function (on) import           Data.Ord                 (Down (Down))-import qualified Data.Vector.Unboxed      as Vector+import qualified Data.Vector      as Vector+import qualified Data.Maybe               as Maybe  import qualified Data.PerfectHash.Hashing as Hashing-import qualified Data.PerfectHash.Lookup  as Lookup+import Data.PerfectHash.Hashing (Hash, ArraySize)+import qualified Data.PerfectHash.Lookup as Lookup+import Data.PerfectHash.Types.Nonces (Nonce)+import qualified Data.PerfectHash.Types.Nonces as Nonces  --- | NOTE: Vector may peform better for these structures, but+data AlgorithmParams = AlgorithmParams {+    getNextNonceCandidate :: Nonce -> Nonce+  , startingNonce :: Nonce+  }+++data NonceOrDirect =+    WrappedNonce Nonce+  | DirectEntry Hashing.SlotIndex++instance Default NonceOrDirect where+  def = WrappedNonce def+++-- | NOTE: Vector might perform better for these structures, but -- the code may not be as clean. data LookupTable a = NewLookupTable {-    redirs :: HashMap Int Int-  , vals   :: HashMap Int a+    nonces :: IntMap NonceOrDirect+  , vals   :: IntMap a   }  +data SingletonBucket a = SingletonBucket Hash a+  deriving Eq+++data HashBucket a = HashBucket {+    _hashVal :: Hash+  , bucketMembers :: [a]+  }++instance Eq (HashBucket a) where +  (==) = (==) `on` (Down . length . bucketMembers)++instance Ord (HashBucket a) where +  compare = compare `on` (Down . length . bucketMembers)+++data SizedList a = SizedList [a] ArraySize++data IntMapAndSize a = IntMapAndSize (IntMap a) ArraySize+++-- | slots for each bucket with the current nonce attempt+data PlacementAttempt a = PlacementAttempt Nonce [SingletonBucket a]+++data PartialSolution a b = PartialSolution (LookupTable b) [SingletonBucket (a, b)]+++-- * Constants+ emptyLookupTable :: LookupTable a-emptyLookupTable = NewLookupTable HashMap.empty HashMap.empty+emptyLookupTable = NewLookupTable mempty mempty  --- | Used to fill empty slots when promoting a HashMap to a Vector-class Defaultable a where-  getDefault :: a+defaultAlgorithmParams :: AlgorithmParams+defaultAlgorithmParams = AlgorithmParams+  (Nonces.mapNonce (+1))+  (Nonces.Nonce 1) -instance Defaultable Int where-  getDefault = 0 +-- * Functions -data HashMapAndSize a b = HashMapAndSize (HashMap a b) Int+toRedirector :: NonceOrDirect -> Int+toRedirector (WrappedNonce (Nonces.Nonce x)) = x+toRedirector (DirectEntry free_slot_index) =+  Lookup.encodeDirectEntry free_slot_index  -convertToVector :: (Vector.Unbox a, Defaultable a) => LookupTable a -> Lookup.LookupTable a+convertToVector+  :: (Default a)+  => LookupTable a+  -> Lookup.LookupTable a convertToVector x = Lookup.LookupTable a1 a2   where     size = length $ vals x-    a1 = Vector.generate size (\z -> HashMap.lookupDefault 0 z $ redirs x)-    a2 = Vector.generate size (\z -> HashMap.lookupDefault getDefault z $ vals x)+    +    vectorizeNonces input = Vector.generate size $+      toRedirector . flip (IntMap.findWithDefault def) input +    a1 = vectorizeNonces $ nonces x ++    vectorizeVals input = Vector.generate size $+      flip (IntMap.findWithDefault def) input++    a2 = vectorizeVals $ 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. -- -- This function is able to fail fast if one of the elements of the bucket--- yields a collision with the new nonce.-attemptNonceRecursive :: Hashing.ToHashableChunks a =>-     HashMapAndSize Int b-  -> Int -- ^ nonce+-- yields a collision when using the new nonce.+attemptNonceRecursive+  :: Hashing.ToHashableChunks a+  => IntMapAndSize b+  -> Nonce   -> IntSet -- ^ occupied slots-  -> [a] -- ^ keys-  -> [Maybe Int]+  -> [(a, b)] -- ^ keys+  -> [Maybe Hashing.SlotIndex] attemptNonceRecursive _ _ _ [] = []-attemptNonceRecursive values_and_size nonce occupied_slots (current_key:remaining_bucket_keys) =+attemptNonceRecursive+    values_and_size+    nonce+    occupied_slots+    ((current_key, _):remaining_bucket_keys) =    if cannot_use_slot     then pure Nothing     else Just slot : recursive_result    where-    HashMapAndSize values size = values_and_size-    slot = Hashing.hashToSlot nonce current_key size+    IntMapAndSize values size = values_and_size+    slot = Hashing.hashToSlot nonce size current_key -    cannot_use_slot = IntSet.member slot occupied_slots || HashMap.member slot values+    Hashing.SlotIndex slotval = slot +    -- TODO: Create a record "SlotOccupation" to encapsulate the IntSet implementation+    cannot_use_slot = IntSet.member slotval occupied_slots || IntMap.member slotval values+     recursive_result = attemptNonceRecursive       values_and_size       nonce-      (IntSet.insert slot occupied_slots)+      (IntSet.insert slotval occupied_slots)       remaining_bucket_keys   -- | Repeatedly try different values of the nonce until we find a hash function -- that places all items in the bucket into free slots. ----- Keeps trying forever, incrementing the candidate nonce by @1@ each time.+-- Increment the candidate nonce by @1@ each time. -- Theoretically we're guaranteed to eventually find a solution.-findNonceForBucket :: Hashing.ToHashableChunks a =>-     Int -- ^ nonce to attempt-  -> HashMapAndSize Int b-  -> [a] -- ^ colliding keys for this bucket-  -> ([(Int, a)], Int) -- ^ slots for each bucket, with the current nonce attempt-findNonceForBucket nonce_attempt values_and_size bucket =+findNonceForBucketRecursive+  :: (Hashing.ToHashableChunks a)+  => AlgorithmParams+  -> Nonce -- ^ nonce to attempt+  -> IntMapAndSize b+  -> [(a, b)] -- ^ colliding keys for this bucket+  -> PlacementAttempt (a, b)+findNonceForBucketRecursive algorithm_params nonce_attempt values_and_size bucket = -  maybe recursive_result (\x -> (zip x bucket, nonce_attempt)) maybe_attempt_result+  -- This is a "lazy" (and awkward) way to specify recursion:+  -- If the result ("result_for_this_iteration") at this iteration of the recursion+  -- is not "Nothing", then, wrap it in a "PlacementAttempt" record.+  -- Otherwise, descend one layer deeper by computing "recursive_result".+  maybe+    recursive_result+    wrapSlotIndicesAsAttempt+    maybe_final_result+   where-    recursive_result = findNonceForBucket (nonce_attempt + 1) values_and_size bucket-    maybe_attempt_result = sequenceA $ attemptNonceRecursive+    wrapSlotIndicesAsAttempt = PlacementAttempt nonce_attempt .+      flip (zipWith SingletonBucket) bucket . map (Hashing.Hash . Hashing.getIndex)++    -- NOTE: attemptNonceRecursive returns a list of "Maybe SlotIndex"+    -- records. If *any* of those elements are Nothing (that is, at+    -- least one of the slots were not successfully placed), then applying+    -- sequenceA to that list will yield Nothing.+    maybe_final_result = sequenceA $ attemptNonceRecursive       values_and_size       nonce_attempt       mempty       bucket +    recursive_result = findNonceForBucketRecursive+      algorithm_params+      (getNextNonceCandidate algorithm_params nonce_attempt)+      values_and_size+      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 :: (Hashing.ToHashableChunks a, Eq a, Hashable a) =>-     HashMapAndSize a b+-- and all previously placed buckets.+processMultiEntryBuckets+  :: (Hashing.ToHashableChunks a)+  => AlgorithmParams+  -> ArraySize   -> LookupTable b-  -> (Int, [a])+  -> HashBucket (a, b)   -> LookupTable b-handleMultiBuckets sized_words_dict old_lookup_table (computed_hash, bucket) =-  NewLookupTable new_g new_values_dict+processMultiEntryBuckets+    algorithm_params+    size+    old_lookup_table+    (HashBucket computed_hash bucket_members) =++  NewLookupTable new_nonces new_values_dict   where-    HashMapAndSize words_dict size = sized_words_dict+    NewLookupTable old_nonces old_values_dict = old_lookup_table -    sized_vals_dict = HashMapAndSize (vals old_lookup_table) size-    (slots_for_bucket, nonce) = findNonceForBucket 1 sized_vals_dict bucket+    sized_vals_dict = IntMapAndSize old_values_dict size -    new_g = HashMap.insert computed_hash nonce $ redirs old_lookup_table-    new_values_dict = foldr fold_func (vals old_lookup_table) slots_for_bucket+    -- This is assured to succeed; it starts with a nonce of 1+    -- but keeps incrementing it until all of the keys in this+    -- bucket are placeable.+    PlacementAttempt nonce slots_for_bucket =+      findNonceForBucketRecursive+        algorithm_params+        (startingNonce algorithm_params)+        sized_vals_dict+        bucket_members -    fold_func (slot_val, bucket_val) = HashMap.insert slot_val $-      HashMap.lookupDefault (error "not found") bucket_val words_dict+    new_nonces = IntMap.insert+      (Hashing.getHash computed_hash)+      (WrappedNonce nonce)+      old_nonces +    new_values_dict = foldr place_values old_values_dict slots_for_bucket --- | 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 :: (Hashing.ToHashableChunks a, Eq a, Hashable a) =>-     HashMapAndSize a b-  -> [(Int, [a])]-  -> (LookupTable b, [(Int, a)])-findCollisionNonces sized_words_dict sorted_bucket_hash_tuples =+    place_values (SingletonBucket slot_val (_, value)) =+      IntMap.insert (Hashing.getHash slot_val) value -  (lookup_table, remaining_words)++-- | This function exploits the sorted structure of the list+-- by skimming the multi-entry buckets from the front of the+-- list. Then we filter the single-entry buckets by dropping+-- the empty buckets.+--+-- The partial solution produced by this function entails+-- all of the colliding nonces as fully placed.+handleCollidingNonces+  :: (Hashing.ToHashableChunks a)+  => AlgorithmParams+  -> ArraySize+  -> SortedList (HashBucket (a, b))+  -> PartialSolution a b+handleCollidingNonces algorithm_params size sorted_bucket_hash_tuples =++  PartialSolution lookup_table non_colliding_buckets   where      -- Since the buckets have been sorted by descending size,     -- once we get to the bucket with 1 or fewer elements,     -- we know there are no more collision buckets.-    (multi_entry_buckets, single_or_fewer_buckets) = span ((> 1) . length . snd) sorted_bucket_hash_tuples+    (multi_entry_buckets, single_or_fewer_buckets) =+      span ((> 1) . length . bucketMembers) $+        fromSortedList sorted_bucket_hash_tuples      -- 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+    lookup_table = foldl'+      (processMultiEntryBuckets algorithm_params size)+      emptyLookupTable+      multi_entry_buckets -    single_entry_buckets = takeWhile (not . null . snd) single_or_fewer_buckets-    remaining_words = map (second head) single_entry_buckets+    non_colliding_buckets = Maybe.mapMaybe+      convertToSingletonBucket+      single_or_fewer_buckets +    convertToSingletonBucket (HashBucket hashVal elements) =+      SingletonBucket hashVal <$> Maybe.listToMaybe elements --- | Sort buckets by descending size-preliminaryBucketPlacement :: (Hashing.ToHashableChunks a, Eq a, Hashable a) =>-     HashMap a b-  -> [(Int, [a])]-preliminaryBucketPlacement words_dict =-  sortOn (Down . length . snd) bucket_hash_tuples++-- | Hash the keys into buckets and sort them by descending size+preliminaryBucketPlacement+  :: (Hashing.ToHashableChunks a)+  => SizedList (a, b)+  -> SortedList (HashBucket (a, b))+preliminaryBucketPlacement sized_list =+  toSortedList bucket_hash_tuples   where-    size = HashMap.size words_dict-    slot_key_pairs = deriveTuples (\k -> Hashing.hashToSlot 0 k size) $ HashMap.keys words_dict+    SizedList tuplified_words_dict size = sized_list -    bucket_hash_tuples = HashMap.toList $ binTuplesBySecond slot_key_pairs+    f = Hashing.getIndex . Hashing.hashToSlot (Nonces.Nonce 0) size . fst +    slot_key_pairs = deriveTuples f tuplified_words_dict +    bucket_hash_tuples = map (uncurry HashBucket . first Hashing.Hash) $+      IntMap.toList $ binTuplesBySecond slot_key_pairs+++-- | Arbitrarily pair the non-colliding buckets with free slots.+--+-- At this point, all of the "colliding" hashes have been resolved+-- to their own slots, so we just take the leftovers.+assignDirectSlots+  :: ArraySize+  -> PartialSolution a b+  -> LookupTable b+assignDirectSlots size (PartialSolution intermediate_lookup_table non_colliding_buckets) =+  NewLookupTable final_nonces final_values+  where+    isUnusedSlot (Hashing.SlotIndex s) =+      not $ IntMap.member s $ vals intermediate_lookup_table++    unused_slots = filter isUnusedSlot $ Hashing.generateArrayIndices size++    zipped_remaining_with_unused_slots =+      zip non_colliding_buckets unused_slots++    insertDirectEntry (SingletonBucket computed_hash _, free_slot_index) =+      -- Observe here that both the output and input+      -- are nonces:+      IntMap.insert (Hashing.getHash computed_hash) $ DirectEntry free_slot_index++    final_nonces = foldr+      insertDirectEntry+      (nonces intermediate_lookup_table)+      zipped_remaining_with_unused_slots++    f2 (SingletonBucket _ (_, map_value), Hashing.SlotIndex free_slot_index) =+      IntMap.insert free_slot_index map_value++    final_values = foldr+      f2+      (vals intermediate_lookup_table)+      zipped_remaining_with_unused_slots++ -- | Generates a minimal perfect hash for a set of key-value pairs. -- -- The keys must be instances of 'Hashing.ToHashableChunks'. -- The values may be of arbitrary type. ----- A 'HashMap' is required as input to guarantee that there are no duplicate keys.-createMinimalPerfectHash :: (Vector.Unbox b, Defaultable b, Hashing.ToHashableChunks a, Eq a, Hashable a) =>-     HashMap a b -- ^ key-value pairs-  -> Lookup.LookupTable b-     -- ^ output for use by 'LookupTable.lookup' or a custom code generator-createMinimalPerfectHash words_dict =-  convertToVector $ NewLookupTable final_g final_values+-- A 'Map' is required as input to guarantee that there are+-- no duplicate keys.+createMinimalPerfectHash+  :: (Hashing.ToHashableChunks k, Default v)+  => Map k v -- ^ key-value pairs+  -> Lookup.LookupTable v+     -- ^ output for use by 'Lookup.lookup' or a custom code generator+createMinimalPerfectHash original_words_dict =+  convertToVector final_solution   where-    size = HashMap.size words_dict+    tuplified_words_dict = Map.toList original_words_dict+    size = Hashing.ArraySize $ length tuplified_words_dict+    sized_list = SizedList tuplified_words_dict size -    sorted_bucket_hash_tuples = preliminaryBucketPlacement words_dict+    sorted_bucket_hash_tuples = preliminaryBucketPlacement sized_list -    (intermediate_lookup_table, remaining_word_hash_tuples) = findCollisionNonces-      (HashMapAndSize words_dict size)+    partial_solution = handleCollidingNonces+      defaultAlgorithmParams+      size       sorted_bucket_hash_tuples -    unused_slots = filter (not . (`HashMap.member` vals intermediate_lookup_table)) [0..(size - 1)]--    zipped_remaining_with_unused_slots = zip remaining_word_hash_tuples unused_slots--    -- We subtract one to ensure it's negative even if the zeroeth slot was used.-    f1 ((computed_hash, _), free_slot_index) = HashMap.insert computed_hash $ Lookup.encodeDirectEntry free_slot_index-    final_g = foldr f1 (redirs intermediate_lookup_table) zipped_remaining_with_unused_slots--    f2 ((_, word), free_slot_index) = HashMap.insert free_slot_index $-      HashMap.lookupDefault (error "Impossible!") word words_dict--    final_values = foldr f2 (vals intermediate_lookup_table) zipped_remaining_with_unused_slots+    final_solution = assignDirectSlots size partial_solution  --- * Utilities+-- * Utility functions  -- | 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+binTuplesBySecond+  :: (Foldable t)+  => t (a, Int)+  -> IntMap [a]+binTuplesBySecond = foldr f mempty   where-    f tuple = HashMap.insertWith (++) (snd tuple) [fst tuple]+    f = uncurry (IntMap.insertWith mappend) .+      fmap pure . swap  --- * Utility functions- -- | duplicates the argument into both members of the tuple duple :: a -> (a, a) duple = join (,)@@ -237,5 +424,5 @@ derivePair g = fmap g . duple  -deriveTuples :: (a -> b) -> [a] -> [(a, b)]-deriveTuples = map . derivePair+deriveTuples :: Functor t => (a -> b) -> t a -> t (a, b)+deriveTuples = fmap . derivePair
src/Data/PerfectHash/Hashing.hs view
@@ -2,7 +2,6 @@  {-# LANGUAGE FlexibleInstances    #-} {-# LANGUAGE Safe                 #-}-{-# LANGUAGE TypeSynonymInstances #-}   -- | Implements the specialized hash function for@@ -14,13 +13,27 @@  import           Data.Binary          (encode) import           Data.Bits            (xor, (.&.))-import           Data.ByteString.Lazy (unpack)+import qualified Data.ByteString.Lazy as B (unpack) import           Data.Char            (ord) import           Data.Foldable        (foldl') import           Data.Text            (Text) import qualified Data.Text            as T+import qualified Data.PerfectHash.Types.Nonces as Nonces+import Data.PerfectHash.Types.Nonces (Nonce) +-- Types +newtype SlotIndex = SlotIndex {getIndex :: Int}++newtype Hash = Hash {getHash :: Int}+  deriving (Eq, Show)++newtype ArraySize = ArraySize Int+  deriving Show+++-- * Constants+ -- | This choice of prime number @0x01000193@ was taken from the Python implementation -- on <http://stevehanov.ca/blog/index.php?id=119 Steve Hanov's page>. primeFNV :: Int@@ -31,30 +44,52 @@ mask32bits = 0xffffffff  +-- * Class instances+ -- | Mechanism for a key to be decomposed into units processable by the -- <http://isthe.com/chongo/tech/comp/fnv/#FNV-1a FNV-1a> hashing algorithm. class ToHashableChunks a where-  toHashableChunks :: a -> [Int]+  toHashableChunks :: a -> [Hash]  instance ToHashableChunks Int where-  toHashableChunks = map fromIntegral . unpack . encode+  toHashableChunks = map (Hash. fromIntegral) . B.unpack . encode +instance ToHashableChunks String where+  toHashableChunks = map $ Hash . ord+ instance ToHashableChunks Text where-  toHashableChunks = map ord . T.unpack+  toHashableChunks = toHashableChunks . T.unpack -instance ToHashableChunks String where-  toHashableChunks = map ord+-- Utilities +generateArrayIndices :: ArraySize -> [SlotIndex]+generateArrayIndices (ArraySize size) = map SlotIndex [0..(size - 1)] ++-- * Main functions+ hashToSlot :: ToHashableChunks a =>-     Int -- ^ nonce+     Nonce+  -> ArraySize   -> a -- ^ key-  -> Int -- ^ array size-  -> Int-hashToSlot nonce key size = hash nonce key `mod` size+  -> SlotIndex+hashToSlot nonce (ArraySize size) key =+  SlotIndex $ getHash (hash nonce key) `mod` size  --- | Uses the \"FNV-1a\" algorithm from the+-- Used in the 'hash' function+getNonzeroNonceVal :: Nonce -> Int+getNonzeroNonceVal (Nonces.Nonce nonce) =+  if nonce == 0+    then primeFNV+    else nonce+++-- | 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.+--+-- Uses the \"FNV-1a\" algorithm from the -- <http://isthe.com/chongo/tech/comp/fnv/#FNV-1a FNV website>: -- -- > hash = offset_basis@@ -62,21 +97,15 @@ -- >         hash = hash xor octet_of_data -- >         hash = hash * FNV_prime -- > return hash------ 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 :: ToHashableChunks a =>-     Int -- ^ nonce+     Nonce -- ^ nonce   -> a -- ^ key-  -> Int+  -> Hash hash nonce =    -- NOTE: This must be 'foldl', not 'foldr'-  foldl' combine d . toHashableChunks+  Hash . foldl' combine d . toHashableChunks   where-    d = if nonce == 0-      then primeFNV-      else nonce+    d = getNonzeroNonceVal nonce -    combine acc = (.&. mask32bits) . (* primeFNV) . xor acc+    combine acc = (.&. mask32bits) . (* primeFNV) . xor acc . getHash
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--- better known as <https://en.wikipedia.org/wiki/Salt_(cryptography) \"salt\">.+-- better known as a \"<https://en.wikipedia.org/wiki/Salt_(cryptography) salt>\". module Data.PerfectHash.Lookup (     LookupTable (LookupTable, nonces, values)   , size@@ -9,11 +9,13 @@   , lookup   ) where -import           Data.Vector.Unboxed      (Vector, (!))-import qualified Data.Vector.Unboxed      as Vector+import           Data.Vector      (Vector, (!))+import qualified Data.Vector      as Vector import           Prelude                  hiding (lookup) +import Data.PerfectHash.Types.Nonces (Nonce (Nonce)) import qualified Data.PerfectHash.Hashing as Hashing+import qualified Data.PerfectHash.Types.Nonces as Nonces   -- | Inputs for the lookup function.@@ -41,18 +43,38 @@   }  -size :: Vector.Unbox a => LookupTable a -> Int-size = Vector.length . values+size :: LookupTable a -> Hashing.ArraySize+size = Hashing.ArraySize . Vector.length . values  -encodeDirectEntry :: Int -> Int-encodeDirectEntry = subtract 1 . negate+-- NOTE: We subtract one to ensure it's negative even if the+-- zeroeth slot was used. This lets us test for "direct encoding"+-- by checking of the value is negative.+encodeDirectEntry :: Hashing.SlotIndex -> Int+encodeDirectEntry (Hashing.SlotIndex val) =+  subtract 1 $ negate val  +-- | NOTE: negation, followed by subtracting 1 is its own self-inverse.+--+-- Example:+-- > a = 7+-- > f(a) = -7 - 1+-- >      = -8+-- >+-- > a' = -8+-- > f(a') = -(-8) - 1+-- >      = 8 - 1+-- >      = 7+decodeDirectEntry :: Int -> Hashing.SlotIndex+decodeDirectEntry val =+  Hashing.SlotIndex $ encodeDirectEntry $ Hashing.SlotIndex val++ -- | For embedded applications, this function would usually be re-implemented -- in C code. ----- == Algorithm description+-- == Procedure description -- The lookup procedure is three steps: -- --     1. Compute the 'Hashing.hash' (with a nonce of zero) of the "key", modulo@@ -68,8 +90,9 @@ --           respect to the length of the 'values' array. -- --     3. Use the result of (2) as the index into the 'values' array.-lookup :: (Hashing.ToHashableChunks a, Vector.Unbox b) =>-     LookupTable b+lookup+  :: (Hashing.ToHashableChunks a)+  => LookupTable b   -> a -- ^ key   -> b -- ^ value lookup lookup_table key =@@ -79,10 +102,10 @@   where     table_size = size lookup_table -    nonce_index = Hashing.hashToSlot 0 key table_size+    Hashing.SlotIndex nonce_index = Hashing.hashToSlot (Nonce 0) table_size key     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.hashToSlot nonce key table_size+    -- Negative nonce value indicates that we don't need extra lookup layer+    Hashing.SlotIndex v_key = if Nonces.isDirectSlot nonce+      then decodeDirectEntry nonce+      else Hashing.hashToSlot (Nonces.Nonce nonce) table_size key
+ src/Data/PerfectHash/Types/Nonces.hs view
@@ -0,0 +1,24 @@+{-# OPTIONS_HADDOCK hide #-}+module Data.PerfectHash.Types.Nonces where++import           Data.Default             (Default, def)+++-- * Types++newtype Nonce = Nonce Int+  deriving Show++instance Default Nonce where+  def = Nonce 0++++-- * Helper functions++mapNonce :: (Int -> Int) -> Nonce -> Nonce+mapNonce f (Nonce x) = Nonce $ f x+++isDirectSlot :: Int -> Bool+isDirectSlot val = val < 0
test-utils/Exercise.hs view
@@ -4,20 +4,24 @@  import           Control.Monad            (unless) import           Data.Foldable            (traverse_)-import           Data.HashMap.Strict      (HashMap)-import qualified Data.HashMap.Strict      as HashMap-import qualified Data.Vector.Unboxed      as Vector+import qualified Data.Map as Map+import           Data.Map                 (Map)+import           Data.IntSet              (IntSet)+import qualified Data.IntSet              as IntSet+import           System.Random            (RandomGen, mkStdGen, random)  import qualified Data.PerfectHash.Hashing as Hashing import qualified Data.PerfectHash.Lookup  as Lookup  -testLookups :: (Show b, Eq b, Show a, Hashing.ToHashableChunks a, Vector.Unbox b) =>-     Lookup.LookupTable b-  -> HashMap a b+-- | genericized to facilitate benchmarking+testLookupsHelper+  :: (Show b, Eq b, Show a, Hashing.ToHashableChunks a)+  => (a -> b) -- ^ lookup function+  -> Map a b   -> Either String ()-testLookups lookup_table =-  traverse_ check_entry . HashMap.toList+testLookupsHelper lookup_function =+  traverse_ check_entry . Map.toList   where     check_entry (word, source_index) = unless (lookup_result == source_index) $       Left $ unwords [@@ -29,18 +33,68 @@         , show source_index         ]       where-        lookup_result = Lookup.lookup lookup_table word+        lookup_result = lookup_function word  +testHashMapLookups+  :: (Show b, Eq b, Show a, Ord a, Hashing.ToHashableChunks a)+  => Map a b+  -> Either String ()+testHashMapLookups hash_map = testLookupsHelper+  (\x -> Map.findWithDefault (error "not found") x hash_map)+  hash_map+++testPerfectLookups+  :: (Show b, Eq b, Show a, Hashing.ToHashableChunks a)+  => Lookup.LookupTable b+  -> Map a b+  -> Either String ()+testPerfectLookups = testLookupsHelper . Lookup.lookup++ -- | Generate a map of words from a file to their line numbers. -- -- Intended for use with @\"/usr/share/dict/words\"@. wordsFromFile :: FilePath -> IO [(String, Int)] wordsFromFile path = do   file_lines <- readFile path-  let word_index_tuples = zip (lines file_lines) [1..]-  return word_index_tuples+  return $ zip (lines file_lines) [1..] ++-- * Random integers++data RandIntAccum t = RandIntAccum+  t -- ^ random number generator+  Int -- ^ max count+  IntSet -- ^ accumulated unique random numbers+++mkIntMapTuples :: Int -> Map Int Int+mkIntMapTuples valueCount = Map.fromList $ zip random_ints [1..]+  where+    seed_value = RandIntAccum (mkStdGen 0) valueCount IntSet.empty+    random_ints = IntSet.toList $ getUniqueRandomIntegers seed_value+++-- | 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)++-- * Other utilities  eitherExit :: Either String b -> IO () eitherExit x = case x of
test/Main.hs view
@@ -2,77 +2,96 @@  module Main where +import           Data.Default                   (Default) import           Data.Either                    (isRight)-import           Data.Hashable                  (Hashable)-import           Data.HashMap.Strict            (HashMap)-import qualified Data.HashMap.Strict            as HashMap+import qualified Data.Map as Map+import           Data.Map                 (Map) import           Data.Text                      (Text)-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  Data.PerfectHash.Hashing       (Hash) import qualified Data.PerfectHash.Hashing       as Hashing+import qualified Data.PerfectHash.Types.Nonces as Nonces+ import qualified Exercise  -testHashComputation :: (Hashing.ToHashableChunks a, Show a) =>-     a-  -> Int+testHashComputation+  :: (Hashing.ToHashableChunks a, Show a)+  => a+  -> Hash   -> IO () testHashComputation key val =   assertEqual error_message val computed_hash   where     error_message = unwords ["Incorrect hash computation of", show key]-    computed_hash = Hashing.hash 0 key+    computed_hash = Hashing.hash (Nonces.Nonce 0) key  -wordIndexTuplesString :: HashMap String Int-wordIndexTuplesString = HashMap.fromList $ zip [+mkInputs+  :: Ord a+  => [a]+  -> Map a Int+mkInputs inputs = Map.fromList $ zip inputs [1..]+++wordIndexTuplesString :: Map String Int+wordIndexTuplesString = mkInputs [     "apple"   , "banana"   , "carrot"-  ] [1..]+  ]  -wordIndexTuplesText :: HashMap Text Int-wordIndexTuplesText = HashMap.fromList $ zip [+wordIndexTuplesText :: Map Text Int+wordIndexTuplesText = mkInputs [     "alpha"   , "beta"   , "gamma"-  ] [1..]+  ]  -intMapTuples :: HashMap Int Int-intMapTuples = HashMap.fromList $ zip [+intMapTuples :: Map Int Int+intMapTuples = mkInputs [     1000   , 5555   , 9876-  ] [1..]+  ]  -testHashLookups :: (Show a, Show b, Eq b, Vector.Unbox b, Construction.Defaultable b, Hashing.ToHashableChunks a, Eq a, Hashable a) =>-     HashMap a b+testHashLookups+  :: (Show a, Show b, Eq b, Default b, Hashing.ToHashableChunks a, Ord a)+  => Map 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+    test_result_either = Exercise.testPerfectLookups lookup_table word_index_tuples   tests = [     testGroup "Hash computation" [-      testCase "compute-string-hash" $ testHashComputation ("blarg" :: String) 3322346319-    , testCase "compute-int-hash" $ testHashComputation (70000 :: Int) 4169891409+      testCase "compute-string-hash" $ testHashComputation ("blarg" :: String) $ Hashing.Hash 3322346319+    , testCase "compute-int-hash" $ testHashComputation (70000 :: Int) $ Hashing.Hash 4169891409     ]   , testGroup "Hash lookups" [       testCase "word-lookups-string" $ testHashLookups wordIndexTuplesString     , testCase "word-lookups-text" $ testHashLookups wordIndexTuplesText     , testCase "int-lookups" $ testHashLookups intMapTuples     ]+  , testGroup "Large scale round-tripping with random inputs" [+      testCase "integers" $ assertBool "Lookups failed to match input" $+        isRight $ Exercise.testPerfectLookups lookup_table intMapTuples+    ]   ]+  where+    intMapTuples = Exercise.mkIntMapTuples 100000+    lookup_table = Construction.createMinimalPerfectHash intMapTuples+       main = defaultMain tests