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)