packages feed

exp-cache 0.1.0.0 → 0.1.0.1

raw patch · 11 files changed

+169/−30 lines, 11 filesPVP: major bump suggested

API removals or changes: PVP suggests a major version bump

API changes (from Hackage documentation)

+ Data.Cache: data FIFO k
+ Data.Cache: newFIFO :: FIFO k
+ Data.Cache: readThrough :: (Hashable k, NFData v, EvictionStrategy s, Eq k, Ord k, Monad m) => Cache k v (s k) -> k -> (k -> m v) -> m (v, Cache k v (s k))
+ Data.Cache.Eviction.FIFO: data FIFO k
+ Data.Cache.Eviction.FIFO: instance Data.Cache.Eviction.EvictionStrategy Data.Cache.Eviction.FIFO.FIFO
+ Data.Cache.Eviction.FIFO: instance GHC.Classes.Eq k => GHC.Classes.Eq (Data.Cache.Eviction.FIFO.FIFO k)
+ Data.Cache.Eviction.FIFO: instance GHC.Show.Show k => GHC.Show.Show (Data.Cache.Eviction.FIFO.FIFO k)
+ Data.Cache.Eviction.FIFO: newFIFO :: FIFO k
- Data.Cache: newCache :: (Hashable k, NFData v, EvictionStrategy s, Eq k, Ord k) => Int -> s k -> Cache k v (s k)
+ Data.Cache: newCache :: (Hashable k, NFData v, EvictionStrategy s, Eq k, Ord k) => Natural -> s k -> Cache k v (s k)

Files

ChangeLog.md view
@@ -1,3 +1,4 @@ # Changelog for lru-cache  ## Unreleased changes+1.0.0.1   - Introduced the FIFO strategy
exp-cache.cabal view
@@ -1,5 +1,5 @@ name:           exp-cache-version:        0.1.0.0+version:        0.1.0.1 description:    Please see the README on Github at <https://github.com/ChrisCoffey/exp-cache#readme> homepage:       https://github.com/ChrisCoffey/exp-cache#readme bug-reports:    https://github.com/ChrisCoffey/exp-cache/issues@@ -38,6 +38,7 @@     , Data.Cache.Eviction.LFU     , Data.Cache.Eviction.MRU     , Data.Cache.Eviction.RR+    , Data.Cache.Eviction.FIFO   other-modules:     Data.Cache.Eviction   default-extensions:  DataKinds FlexibleContexts ScopedTypeVariables OverloadedStrings ViewPatterns NamedFieldPuns@@ -51,6 +52,9 @@   hs-source-dirs:       test   ghc-options: -threaded -rtsopts -with-rtsopts=-N+  default-extensions:  DataKinds FlexibleContexts ScopedTypeVariables OverloadedStrings ViewPatterns NamedFieldPuns+                       KindSignatures RecordWildCards ConstraintKinds TypeSynonymInstances FlexibleInstances MultiParamTypeClasses+                       InstanceSigs FunctionalDependencies TupleSections   build-depends:       base >=4.7 && <5     , exp-cache@@ -64,6 +68,7 @@       Spec.LRUTests,       Spec.MRUTests,       Spec.RRTests,+      Spec.LFUTests,       Spec.CacheTests   default-language: Haskell2010 
src/Data/Cache.hs view
@@ -1,6 +1,7 @@ module Data.Cache (     Cache,     newCache,+    readThrough,      EvictionStrategy(..), @@ -21,7 +22,11 @@      -- | Least Frequently Used Cache     LFU,-    newLFU+    newLFU,++    -- | First in First Out Cache (a queue)+    FIFO,+    newFIFO     ) where  import Data.Cache.Eviction (EvictionStrategy(..))@@ -29,58 +34,73 @@ import Data.Cache.Eviction.MRU import Data.Cache.Eviction.RR import Data.Cache.Eviction.LFU+import Data.Cache.Eviction.FIFO  import qualified Data.HashMap.Strict as HM import Control.DeepSeq (NFData) import Data.Hashable (Hashable)+import Numeric.Natural +-- | A Cache is a bounded collection of data that supports fast random access. When the cach is full, the associated+-- 'EvictionStrategy' is used to discard an item from the cache. Library users should use 'readThrough' to read and+-- replace items in the cache. data Cache k v s =     Cache {         cacheData :: HM.HashMap k v,         evictionStrategy :: s,-        maxSize :: Int,-        currentSize :: Int+        maxSize :: !Int,+        currentSize :: !Int         } +-- | Create a new cache of the specified size using the provided 'EvictionStrategy' newCache :: (Hashable k, NFData v, EvictionStrategy s, Eq k, Ord k) =>-    Int -- ^ The maximum cache size+    Natural -- ^ The maximum cache size     -> s k -- ^ The evictionStrategy     -> Cache k v (s k)+newCache 0 _ = error "Invalid cache size" newCache maxSize evictionStrategy =     Cache {         cacheData = HM.empty,         evictionStrategy,-        maxSize,+        maxSize = fromIntegral maxSize,         currentSize = 0     } +-- | Performs a read-through operation on a given cache. readThrough :: (Hashable k, NFData v, EvictionStrategy s, Eq k, Ord k, Monad m) =>-    Cache k v (s k)-    -> k-    -> (k -> m v)+    Cache k v (s k) -- ^ The current cache state+    -> k            -- ^ The key to look up in the cache+    -> (k -> m v)   -- ^ The accessor function to evaluate if the key is not found     -> m (v , Cache k v (s k))-readThrough cache@(Cache {maxSize, evictionStrategy, cacheData, currentSize}) key onMiss =+readThrough cache@(Cache {maxSize, cacheData, currentSize}) key onMiss =     case HM.lookup key cacheData of-        -- A hit. Because the value is already in the cache, no need to evict. Update the-        -- accessed time for 'key'-        Just v -> do-            let strat' = recordLookup key evictionStrategy-            pure (v, cache {evictionStrategy = strat'} )         -- On a miss when the cache is full:         -- 1) evict the relevant key (removes from HashMap & Strategy)         -- 2) Record the newest key in the strategy         -- 3) Add key to the cache data-        Nothing | maxSize == currentSize -> do+        Nothing | maxSize <= currentSize -> do             v <- onMiss key-            let (strat', evicted) = evict evictionStrategy-                strat'' = recordLookup key strat'-                cacheData' = HM.insert key v $ maybe cacheData (`HM.delete` cacheData) evicted-            pure (v, cache {cacheData = cacheData', evictionStrategy = strat''})+            let cache' = postEviction v+            pure (v, cache' {currentSize = currentSize + 1})+        -- A hit. Because the value is already in the cache, no need to evaluate onMiss. Update the+        -- accessed time for 'key'+        Just v -> do+            let cache' = if maxSize <= currentSize+                     then postEviction v+                     else cache+                strat' = recordLookup key (evictionStrategy cache')+            pure (v, cache' {evictionStrategy = strat'} )         -- A miss when the cache is not full         -- 1) Record the new key in the data cache         -- 2) Record the new key in the strategy         Nothing -> do             v <- onMiss key-            let strat' = recordLookup key evictionStrategy+            let strat' = recordLookup key (evictionStrategy cache)                 cacheData' = HM.insert key v cacheData-            pure (v, cache {cacheData = cacheData', evictionStrategy = strat'})+            pure (v, cache {cacheData = cacheData', evictionStrategy = strat', currentSize = currentSize + 1})+        where+            postEviction v = let+                (strat', evicted) = evict (evictionStrategy cache)+                strat'' = recordLookup key strat'+                cacheData' = HM.insert key v $ maybe cacheData (`HM.delete` cacheData) evicted+                in cache {cacheData = cacheData', evictionStrategy = strat''}
+ src/Data/Cache/Eviction/FIFO.hs view
@@ -0,0 +1,23 @@+module Data.Cache.Eviction.FIFO (+    FIFO,+    newFIFO+) where++import Data.Cache.Eviction+import Data.Sequence++-- | A First in First Out eviction strategy. Items are evicted based on order added, regardless of usage patterns.+newtype FIFO k = FIFO (Seq k)+    deriving (Eq, Show)++newFIFO :: FIFO k+newFIFO = FIFO empty++instance EvictionStrategy FIFO where+    recordLookup key (FIFO keys) =+        FIFO (key <| keys)++    evict (FIFO keys) =+        case viewr keys of+            EmptyR -> (FIFO keys, Nothing)+            rest :> last -> (FIFO rest, Just last)
src/Data/Cache/Eviction/LFU.hs view
@@ -10,6 +10,8 @@ import Data.Hashable import qualified Data.HashPSQ as PSQ +-- | Evict the least frequently used element from the cache. This means as an element is accessed, its "score" increases+-- and the element is more likely to survive eviction once the cache fills up. newtype LFU k = LFU {     queue :: PSQ.HashPSQ k Word64 ()     } deriving (Eq, Show)
src/Data/Cache/Eviction/LRU.hs view
@@ -37,7 +37,7 @@             EmptyR -> (SeqLRU elements, Nothing)             rest :> last -> (SeqLRU rest, Just last) --- | An optimized version of an LRU cache+-- | An optimized version of an LRU cache. The least recently used element in the cache is evicted once the cache fills up. data LRU k =     LRU {         queue :: PSQ.HashPSQ k Word64 (),
src/Data/Cache/Eviction/MRU.hs view
@@ -13,6 +13,7 @@ import Data.Word (Word64) import qualified Data.HashPSQ as PSQ +-- | A Most Recently Used cache. This evicts the most recently accessed item from the cache data MRU k =     MRU {         queue :: PSQ.HashPSQ k Word64 (),
src/Data/Cache/Eviction/RR.hs view
@@ -12,11 +12,12 @@ import System.Random (StdGen, randomR)  -- | Random Replacement cache. The seed is fixed to an 'StdGen' since its both--- easily accessible & good enough for this purpose.+-- easily accessible & good enough for this purpose. Random replacement means exactly what it+-- sounds like: when the cache fills up a random element is selected and evicted. data RR k = RR {-    seed :: StdGen,-    writeCell :: Int,-    upperBound :: Int,+    seed :: !StdGen,+    writeCell :: !Int,+    upperBound :: !Int,     overwritten :: Maybe k, -- This is a wart used for tracking evicted nodes     contents :: StupidBiMap k     } deriving (Show)@@ -26,6 +27,7 @@              upperBound l == upperBound r &&              contents l == contents r +-- | Generate a new Random Replacement cache using the provided seed & size. newRR ::     StdGen     -> Int
test/Spec.hs view
@@ -2,6 +2,7 @@ import Spec.MRUTests (mruTests) import Spec.RRTests (rrSpec) import Spec.LFUTests (lfuTests)+import Spec.CacheTests (cacheTests)  import Test.Tasty  (TestTree, defaultMain, testGroup) @@ -10,6 +11,7 @@  allTests :: TestTree allTests = testGroup "Data.Cache" [+    cacheTests,     lruTests,     mruTests,     rrSpec,
test/Spec/CacheTests.hs view
@@ -4,11 +4,55 @@  import Data.Cache -import           Test.HUnit                   ((@=?))-import           Test.Tasty                   (TestTree, testGroup)-import           Test.Tasty.HUnit             (testCase)+import Control.Exception+import Data.IORef (IORef, newIORef, readIORef, writeIORef)+import Test.HUnit                   ((@=?))+import Test.Tasty                   (TestTree, testGroup)+import Test.Tasty.HUnit             (testCase) +type DefaultCache = Cache Int Int (FIFO Int)  cacheTests :: TestTree cacheTests = testGroup "Cache" [+    testCase "Cannot initialize a 0 sized cache" $ do+        res <- catch ((pure $! (newCache 0 newFIFO :: DefaultCache)) *> pure False) (\(e::SomeException) -> pure True)+        res @=? True+    ,testCase "Verify that a cache size of 1 stores an element before eviction" $ do+        r <- newIORef 0+        let cache = newCache 1 newFIFO+        (v,cache') <- readThrough cache 10 (const $ pure 1 <* writeIORef r 1) :: IO (Int, DefaultCache)+        _ <- readThrough cache' 10 (const $ pure 1 <* writeIORef r 2)+        calledLookup <- readIORef r+        calledLookup @=? 1+    ,testCase "Verify that the proper eviction strategy is evaluated (FIFO vs. LRU)" $ do+        let cache = newCache 1 newFIFO+        r <- newIORef False+        (_,cache') <- readThrough cache 10 (const $ pure 1) :: IO (Int, DefaultCache)+        (_,cache'') <- readThrough cache' 11 (const $ pure 1)+        _ <- readThrough cache'' 10 (const $ pure 1 <* writeIORef r True)+        v <- readIORef r+        v @=? True+    ,testCase "Verify that eviction only fires once the cache is full" $ do+        let cache = newCache 1 newFIFO+        r <- newIORef False+        (_,cache') <- readThrough cache 10 (const $ pure 1) :: IO (Int, DefaultCache)+        (_,cache'') <- readThrough cache' 11 (const $ pure 1)+        _ <- readThrough cache'' 10 (const $ pure 1 <* writeIORef r True)+        v <- readIORef r+        v @=? True+    ,testCase "Verify that the read function evaluates only if the key is not in the cache" $ do+        let cache = newCache 100 newFIFO+        r <- newIORef True+        (_,cache') <- readThrough cache 10 (const $ pure 1) :: IO (Int, DefaultCache)+        _ <- readThrough cache' 10 (const $ pure 1 <* writeIORef r False)+        v <- readIORef r+        v @=? True+    ,testCase "Verify that the value returned by the read function is stored in the cache" $ do+        let cache = newCache 100 newFIFO+        r <- newIORef True+        (v,cache') <- readThrough cache 10 (const $ pure 1) :: IO (Int, DefaultCache)+        v @=? 1     ]++--TODO make a note that this is not the most memory efficient, but it is time efficient+
+ test/Spec/LFUTests.hs view
@@ -0,0 +1,39 @@+module Spec.LFUTests (+    lfuTests+) where++import Data.Cache+import Data.Cache.Eviction.LFU++import           Test.HUnit                   ((@=?))+import           Test.Tasty                   (TestTree, testGroup)+import           Test.Tasty.HUnit             (testCase)+++lfuTests :: TestTree+lfuTests = testGroup "Least Frequently Used" [+    testCase "evict empty == empty" $ do+        let strat = newLFU :: LFU Int+            (s, Nothing) = evict strat :: (LFU Int, Maybe Int)+        s @=? strat+    , testCase "evict (1:empty) == (empty, Just 1) " $ do+        let strat = newLFU :: LFU Int+            s = recordLookup (1 :: Int) strat+            (s', Just _) = evict s :: (LFU Int, Maybe Int)+        LFUContentsOnlyEq s' @=? LFUContentsOnlyEq strat+    , testCase "read a b a => evict b a" $ do+        let strat = newLFU :: LFU Int+            [x,y] = [1,2] :: [Int]+            s = recordLookup x . recordLookup y $ recordLookup x strat+            (s', Just a) = evict s+            (s'', Just b) = evict s'+            (_, Nothing) = evict s'' :: (LFU Int, Maybe Int)+        a == y @=? True+        b == x @=? True+    , testCase "read a b c c b => evict a" $ do+        let strat = newLFU :: LFU Int+            [v,w,x,y,z] = [1,2,3,3,2] :: [Int]+            s = recordLookup v . recordLookup w . recordLookup x . recordLookup y $ recordLookup z strat+            (s', Just a) = evict s:: (LFU Int, Maybe Int)+        a @=? v+    ]