packages feed

net-mqtt-0.8.6.0: src/Data/Map/Strict/Expiring.hs

{-# LANGUAGE DeriveFunctor   #-}
{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE TupleSections   #-}
module Data.Map.Strict.Expiring (
    Map,
    new,
    generation,
    newGen,

    insert,
    delete,
    lookup,
    updateLookupWithKey,
    assocs,

    -- * for testing
    inspect
) where

import           Data.Foldable   (fold)
import qualified Data.Map.Strict as Map
import           Data.Maybe      (fromMaybe, mapMaybe)
import           Data.Set        (Set)
import qualified Data.Set        as Set
import           Prelude         hiding (lookup, map)

data Entry g a = Entry {
  value :: !a,
  gen   :: !g
} deriving (Functor, Show)

-- | A map of values that expire after a given generation.
data Map g k a = Map {
  -- | Primary store of values.
  map        :: !(Map.Map k (Entry g a)),
  -- | The current generation
  generation :: !g,
  -- | A map of generations to keys that are expiring at that generation.
  aging      :: !(Map.Map g (Set k))
} deriving (Functor, Show)

instance Ord g => Foldable (Map g k) where
    foldMap f = foldMap (f . value) . Map.elems . map

-- | Make a new empty Map at the starting generation.
new :: g -> Map g k a
new g = Map Map.empty g Map.empty

-- | 𝑂(log𝑛). Assign the next generation and expire any data this new generation invalidates.
-- The generation may never decrease.  Attempts to decrease it are ignored.
newGen :: (Ord k, Ord g) => g -> Map g k a -> Map g k a
newGen g m
    | g > generation m = expire m { generation = g }
    | otherwise = m

-- | 𝑂(log𝑛). Insert a new value into the map to expire after the given generation.
-- alterF :: (Functor f, Ord k) => (Maybe a -> f (Maybe a)) -> k -> Map k a -> f (Map k a)
insert :: (Ord k, Ord g) => g -> k -> a -> Map g k a -> Map g k a
insert g _ _ m | g < generation m = m
insert g k v m@Map{..} = case Map.alterF (, Just (Entry v g)) k map of
    (Just old, m') -> m{map=m', aging = Map.insertWith (<>) g (Set.singleton k) (removeAging (gen old) k aging)}
    (Nothing, m')  -> m{map=m', aging = Map.insertWith (<>) g (Set.singleton k) aging}

-- | 𝑂(log𝑛). Lookup and update.
-- The function returns changed value, if it is updated. Returns the original key value if the map entry is deleted.
updateLookupWithKey :: (Ord g, Ord k) => g -> (k -> a -> Maybe a) -> k -> Map g k a -> (Maybe a, Map g k a)
updateLookupWithKey g _ _ m | g < generation m = (Nothing, m)
updateLookupWithKey g f k m@Map{..} = case Map.alterF f' k map of
    ((Nothing, _), m')   -> (Nothing, m)
    ((Just old, Nothing), m')  -> (Just (value old), m{map=m', aging = removeAging (gen old) k aging})
    ((Just old, Just new), m') -> (Just (value new), m{map=m', aging = Map.insertWith (<>) g (Set.singleton k) (removeAging (gen old) k aging)})
    where
        f' Nothing = ((Nothing, Nothing), Nothing)
        f' (Just e) = case f k (value e) of
            Nothing -> ((Just e, Nothing), Nothing)
            Just v  -> ((Just e, Just (Entry v g)), Just (Entry v g))

removeAging :: (Ord g, Ord k) => g -> k -> Map.Map g (Set k) -> Map.Map g (Set k)
removeAging g k = Map.update (nonNull . Set.delete k) g
    where nonNull s = if Set.null s then Nothing else Just s

-- | 𝑂(log𝑛). Lookup a value in the map.
-- This will not return any items that have expired.
lookup :: (Ord k, Ord g) => k -> Map g k a -> Maybe a
lookup k = fmap value . Map.lookup k . map

-- | 𝑂(log𝑛). Delete an item.
delete :: (Ord k, Ord g) => k -> Map g k a -> Map g k a
delete k m@Map{..} = case Map.lookup k map of
  Nothing        -> m
  Just Entry{..} -> m { map = Map.delete k map, aging = removeAging gen k aging }

-- | 𝑂(𝑛). Return all current key/value associations.
assocs :: Ord g => Map g k a -> [(k,a)]
assocs Map{..} = fmap value <$> Map.assocs map

-- | 𝑂(log𝑛).  Expire older generation items.
expire :: (Ord g, Ord k) => Map g k a -> Map g k a
expire m@Map{..} = m{ map = map', aging = aging'}
    where
        (todo, exact, later) = Map.splitLookup generation aging
        aging' = later <> maybe mempty (Map.singleton generation) exact
        map' = foldr Map.delete map (fold todo)

-- | Inspect stored size for testing.
inspect :: Ord k => Map g k a -> (Int, g, Int)
inspect Map{..} = (Map.size map, generation, length $ fold aging)