packages feed

git-annex-10.20261113: Utility/LRU.hs

{- LRU
 -
 - Copyright 2026 Joey Hess <id@joeyh.name>
 -
 - License: BSD-2-clause
 -}

{-# LANGUAGE StrictData, BangPatterns #-}

module Utility.LRU (
	LRU,
	emptyLRU,
	listLRU,
	addLRU,
	removeLRU,
	popLRU,
	mapLRU,
	testLRU,
	prop_LRU_sane,
) where

import qualified Data.Map.Strict as M

data LRU k v = LRU
	{ lruFirst :: Maybe k
	, lruLast :: Maybe k
	, lruMap :: M.Map k (LinkedVal k v)
	}
	deriving (Show, Eq)

data LinkedVal k v = LinkedVal
	{ value :: v
	, prev :: Maybe k
	, next :: Maybe k
	}
	deriving (Show, Eq)

setvalue :: Ord k => v -> k -> M.Map k (LinkedVal k v) -> M.Map k (LinkedVal k v)
setvalue v = M.adjust (\linkedval -> linkedval { value = v })

setprev :: Ord k => Maybe k -> k -> M.Map k (LinkedVal k v) -> M.Map k (LinkedVal k v)
setprev v = M.adjust (\linkedval -> linkedval { prev = v })

setnext :: Ord k => Maybe k -> k -> M.Map k (LinkedVal k v) -> M.Map k (LinkedVal k v)
setnext v = M.adjust (\linkedval -> linkedval { next = v })

emptyLRU :: LRU k v
emptyLRU = LRU Nothing Nothing M.empty

listLRU :: Ord k => LRU k v -> [(k, v)]
listLRU lru = case lruFirst lru of
	Nothing -> []
	Just k -> walk k
  where
	walk k = case M.lookup k (lruMap lru) of
		Nothing -> []
		Just linkedval -> (k, value linkedval) :
			case prev linkedval of
				Just k' -> walk k'
				Nothing -> []

removeLRU :: Ord k => LRU k v -> k -> LRU k v
removeLRU lru k = case M.lookup k (lruMap lru) of
	Nothing -> lru
	Just linkedval -> 
		let !lru' = addLRU lru k (value linkedval)
		in lru'
			{ lruLast = case lruLast lru' of
				Nothing -> Nothing
				Just k' -> case M.lookup k' (lruMap lru') of
					Nothing -> Nothing
					Just linkedval' -> next linkedval'
			, lruFirst = case lruFirst lru' of
				Just k' | k' /= k -> Just k'
				_ -> Nothing
			, lruMap = M.delete k $ unsetprev $ lruMap lru'
			}
  where
	unsetprev m = case M.lookup k m of
		Nothing -> m
		Just linkedval -> case next linkedval of
			Nothing -> m
			Just k' -> setprev Nothing k' m

{- When the key already exists in the LRU, its value is updated, and it is
 - moved to last. -}
addLRU :: Ord k => LRU k v -> k -> v -> LRU k v
addLRU lru k v
	| lruLast lru == Just k = lru
		{ lruMap = setvalue v k (lruMap lru)
		}
	| otherwise = LRU
		{ lruFirst = newfirst
		, lruLast = Just k
		, lruMap = 
			let !linkedval = LinkedVal
				{ value = v
				, prev = Nothing
				, next = lruLast lru
				}
			in updatefirst $ M.insert k linkedval $
				skipover $ updateprev $ lruMap lru
		}
  where
	newfirst = case lruFirst lru of
		Nothing -> Just k
		Just k'
			| k' /= k -> Just k'
			| otherwise -> case M.lookup k' (lruMap lru) of
				Nothing -> Nothing
				Just linkedval -> prev linkedval
	updateprev m = case lruLast lru of
		Nothing -> m
		Just k' -> setprev (Just k) k' m
	updatefirst m = case newfirst of
		Nothing -> m
		Just k' -> setnext Nothing k' m
	skipover m = case M.lookup k m of
		Nothing -> m
		Just linkedval -> case (prev linkedval, next linkedval) of
			(Just prevk, Just nextk) -> 
				setprev (Just prevk) nextk $
					setnext (Just nextk) prevk m
			_  -> m

popLRU :: Ord k => LRU k v -> (LRU k v, Maybe (k, v))
popLRU lru = case lruFirst lru of
	Nothing -> (lru, Nothing)
	Just first ->
		case M.lookup first (lruMap lru) of
			Nothing -> (lru, Nothing)
			Just linkedval ->
				let !lru' = LRU
					{ lruFirst = prev linkedval
					, lruLast = case lruLast lru of
						Just k | k == first -> Nothing
						_ -> lruLast lru
					, lruMap = skipover linkedval $
						M.delete first (lruMap lru)
					}
				in (lru', Just (first, value linkedval))
  where
	skipover linkedval m = case prev linkedval of
		Just k -> setnext Nothing k m
		Nothing -> m

mapLRU :: (Ord k, Ord k') => (k -> k') -> (v -> v') -> LRU k v -> LRU k' v'
mapLRU kf vf lru = LRU
	{ lruFirst = kf <$> lruFirst lru
	, lruLast = kf <$> lruLast lru
	, lruMap = M.fromList $ map go $ M.toList $ lruMap lru
	}
  where
	go (k, linkedval) = 
		(kf k, linkedval
			{ value = vf (value linkedval)
			, prev = kf <$> prev linkedval
			, next = kf <$> next linkedval
			})

prop_LRU_sane :: Bool
prop_LRU_sane = and testLRU

testLRU :: [Bool]
testLRU =
	[ listLRU (fst (popLRU l1)) == []
	, listLRU l1 == [(1, foo)]
	, listLRU l2 == [(1, foo), (2, bar)]
	, listLRU l3 == [(1, foo),(2, bar),(3, baz)]
	, listLRU l4 == [(1, foo),(2, bar),(3, baz), (4, new)]
	, popLRU l1 == (emptyLRU, Just (1, foo))
	, popLRU l2 == (l2no1, Just (1, foo))
	, popLRU l3 == (l3no1, Just (1, foo))
	, popLRU (fst (popLRU l1)) == (emptyLRU, Nothing)
	, popLRU (fst (popLRU (fst (popLRU l2)))) == (emptyLRU, Nothing)
	, addLRU l3 1 new == addLRU l3no1 1 new
	, addLRU l3 2 new == addLRU (addLRU l1 3 baz) 2 new
	, addLRU l3 3 new == addLRU l2 3 new
	, removeLRU emptyLRU 1 == (emptyLRU :: LRU Int String)
	, removeLRU l1 1 == emptyLRU
	, removeLRU l2 3 == l2
	, removeLRU l3 3 == l2
	, removeLRU l3 1 == l3no1
	, removeLRU l3 2 == addLRU l1 3 baz
	, removeLRU l2 2 == l1
	, removeLRU l2 1 == l2no1
	, removeLRU l4 4 == l3
	, removeLRU l4 2 == addLRU (addLRU l1 3 baz) 4 new
	, removeLRU l4 3 == addLRU l2 4 new
	, removeLRU l4 1 == addLRU l3no1 4 new
	]
  where
	l1 = addLRU emptyLRU (1 :: Int) foo
	l2 = addLRU l1 2 bar
	l3 = addLRU l2 3 baz
	l4 = addLRU l3 4 new
	l2no1 = addLRU emptyLRU 2 bar
	l3no1 = addLRU l2no1 3 baz
	foo = "foo"
	bar = "bar"
	baz = "baz"
	new = "new"