module Data.Cache ( Cache, newCache, readThrough, EvictionStrategy(..), -- The sequential LRU implementation SeqLRU, newSeqLRU, -- Actual LRU implementation LRU, newLRU, -- An MRU implementation based on the LRU implementation MRU, newMRU, -- Random Replacement cache (RR) RR, newRR, -- | Least Frequently Used Cache LFU, newLFU, -- | First in First Out Cache (a queue) FIFO, newFIFO ) where import Data.Cache.Eviction (EvictionStrategy(..)) import Data.Cache.Eviction.LRU 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 } -- | Create a new cache of the specified size using the provided 'EvictionStrategy' newCache :: (Hashable k, NFData v, EvictionStrategy s, Eq k, Ord k) => 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 = 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) -- ^ 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, cacheData, currentSize}) key onMiss = case HM.lookup key cacheData of -- 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 v <- onMiss key 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 cache) cacheData' = HM.insert key v cacheData 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''}