packages feed

network-control-0.1.5: Network/Control/LRUCache.hs

{-# LANGUAGE RecordWildCards #-}

module Network.Control.LRUCache (
    -- * LRU cache
    LRUCache,
    empty,
    insert,
    delete,
    lookup,
    lookup',

    -- * IO
    LRUCacheRef,
    newLRUCacheRef,
    cached,
    cached',

    -- * Internal
    empty',
) where

import Data.IORef (IORef, atomicModifyIORef', newIORef)
import Data.Int (Int64)
import Data.OrdPSQ (OrdPSQ)
import qualified Data.OrdPSQ as PSQ
import Prelude hiding (lookup)

----------------------------------------------------------------

type Priority = Int64

-- | Sized cache based on least recently used.
data LRUCache k v = LRUCache
    { lcLimit :: Int
    -- ^ The maximum number of elements in the queue
    , lcTick :: Priority
    -- ^ The next logical time
    , lcQueue :: OrdPSQ k Priority v
    }
    deriving (Eq, Show)

----------------------------------------------------------------

-- | Empty 'LRUCache'. /O(1)/
empty
    :: Int
    -- ^ The size of 'LRUCache'.
    -> LRUCache k v
empty capacity
    | capacity < 1 = error "LRUCache.empty: capacity < 1"
    | otherwise =
        LRUCache
            { lcLimit = capacity
            , lcTick = 0
            , lcQueue = PSQ.empty
            }

-- | Empty 'LRUCache'. /O(1)/
empty'
    :: Int
    -- ^ The size of 'LRUCache'.
    -> Int64
    -- ^ Counter
    -> LRUCache k v
empty' capacity tick =
    LRUCache
        { lcLimit = capacity
        , lcTick = tick
        , lcQueue = PSQ.empty
        }

----------------------------------------------------------------

trim :: Ord k => LRUCache k v -> LRUCache k v
trim c@LRUCache{..}
    | lcTick == maxBound =
        let siz = fromIntegral $ PSQ.size lcQueue
            diff = (maxBound :: Priority) - siz
            psq = PSQ.unsafeMapMonotonic (\_ p v -> (p - diff, v)) lcQueue
         in LRUCache
                { lcLimit = lcLimit
                , lcTick = siz
                , lcQueue = psq
                }
    | PSQ.size lcQueue > lcLimit = c{lcQueue = PSQ.deleteMin lcQueue}
    | otherwise = c

----------------------------------------------------------------

-- | Inserting. /O(log n)/
insert :: Ord k => k -> v -> LRUCache k v -> LRUCache k v
insert key val c@LRUCache{..} = trim c'
  where
    queue = PSQ.insert key lcTick val lcQueue
    c' = c{lcTick = lcTick + 1, lcQueue = queue}

----------------------------------------------------------------

-- | Deleting. /O(log n)/
delete :: Ord k => k -> LRUCache k v -> LRUCache k v
delete k c@LRUCache{..} = c{lcQueue = q}
  where
    q = PSQ.delete k lcQueue

----------------------------------------------------------------

-- | Looking up. /O(log n)/
lookup :: Ord k => k -> LRUCache k v -> Maybe v
lookup k LRUCache{..} = snd <$> PSQ.lookup k lcQueue

-- | Looking up and changing priority. /O(log n)/
lookup' :: Ord k => k -> LRUCache k v -> Maybe (v, LRUCache k v)
lookup' k c@LRUCache{..} = case PSQ.alter lookupAndBump k lcQueue of
    (Nothing, _) -> Nothing
    (Just v, q) ->
        let c' = trim $ c{lcTick = lcTick + 1, lcQueue = q}
         in Just (v, c')
  where
    lookupAndBump Nothing = (Nothing, Nothing)
    -- setting its priority to lcTick
    lookupAndBump (Just (_p, v)) = (Just v, Just (lcTick, v))

----------------------------------------------------------------

newtype LRUCacheRef k v = LRUCacheRef (IORef (LRUCache k v))

newLRUCacheRef :: Int -> IO (LRUCacheRef k v)
newLRUCacheRef capacity = LRUCacheRef <$> newIORef (empty capacity)

cached' :: Ord k => LRUCacheRef k v -> k -> IO (Maybe v)
cached' (LRUCacheRef ref) k = do
    atomicModifyIORef' ref $ \c -> case lookup' k c of
        Nothing -> (c, Nothing)
        Just (v, c') -> (c', Just v)

cached :: Ord k => LRUCacheRef k v -> k -> IO v -> IO (v, Bool)
cached (LRUCacheRef ref) k io = do
    lookupRes <- atomicModifyIORef' ref $ \c -> case lookup' k c of
        Nothing -> (c, Nothing)
        Just (v, c') -> (c', Just v)
    case lookupRes of
        Just v -> return (v, True)
        Nothing -> do
            v <- io
            atomicModifyIORef' ref $ \c -> (insert k v c, ())
            return (v, False)