overeasy-0.1.0: src/Overeasy/Assoc.hs
{-# LANGUAGE DeriveAnyClass #-}
-- | See 'Assoc'.
module Overeasy.Assoc
( Assoc
, assocFwd
, assocBwd
, assocEquiv
, assocSize
, assocNew
, assocSingleton
, AssocInsertRes (..)
, assocInsertInc
, assocInsert
, assocFromList
, assocToList
, assocMember
, assocLookupByKey
, assocPartialLookupByKey
, assocLookupByValue
, assocPartialLookupByValue
, assocLookupRoot
, assocRoots
, assocLeaves
, assocMembers
, assocCanCompact
, assocCompactInc
, assocCompact
, assocRemoveAllInc
, assocRemoveAll
, assocUnion
, assocFootprint
) where
import Control.DeepSeq (NFData)
import Control.Monad.State.Strict (MonadState (..), State, modify')
import Data.Coerce (Coercible)
import Data.Foldable (foldl')
import Data.Hashable (Hashable)
import Data.HashMap.Strict (HashMap)
import qualified Data.HashMap.Strict as HashMap
import Data.Maybe (fromJust)
import GHC.Generics (Generic)
import IntLike.Map (IntLikeMap)
import qualified IntLike.Map as ILM
import IntLike.Set (IntLikeSet)
import qualified IntLike.Set as ILS
import Overeasy.EquivFind (EquivAddRes (..), EquivFind, efAddInc, efBwd, efCanCompact, efCompactInc, efEquivs, efLeaves,
efLookupRoot, efMember, efMembers, efNew, efRemoveAllInc, efRoots, efSingleton,
efUnsafeAddLeafInc, efUnsafeMerge)
-- | Associates keys and values in such a way that inserting
-- duplicate values induces equivalences on their keys.
-- Invariant: fwd and bwd maps contain only root keys.
data Assoc x a = Assoc
{ assocFwd :: !(IntLikeMap x a)
-- ^ Map from id to element
, assocBwd :: !(HashMap a x)
-- ^ Map from element to id
, assocEquiv :: !(EquivFind x)
-- ^ Equivalence classes of ids
} deriving stock (Eq, Show, Generic)
deriving anyclass (NFData)
-- | How many values are in the map?
assocSize :: Assoc x a -> Int
assocSize = ILM.size . assocFwd
-- | Creates an empty assoc
assocNew :: Assoc x a
assocNew = Assoc ILM.empty HashMap.empty efNew
-- | Creates a singleton assoc
assocSingleton :: (Coercible x Int, Hashable a) => x -> a -> Assoc x a
assocSingleton x a = Assoc (ILM.singleton x a) (HashMap.singleton a x) (efSingleton x)
-- | The result of inserting into the assoc, if you're interested.
data AssocInsertRes x =
AssocInsertResUnchanged
| AssocInsertResCreated
| AssocInsertResUpdated
| AssocInsertResMerged !(IntLikeSet x)
deriving stock (Eq, Show)
-- | Insert into the assoc (raw version)
assocInsertInc :: (Coercible x Int, Ord x, Eq a, Hashable a) => x -> a -> Assoc x a -> ((x, AssocInsertRes x), Assoc x a)
assocInsertInc x a1 assoc@(Assoc fwd bwd equiv) = finalRes where
finalRes =
let (res, equiv') = efAddInc x equiv
in case res of
EquivAddResNewRoot -> insertRoot x equiv'
EquivAddResAlreadyLeafOf z -> updateRoot z
EquivAddResAlreadyRoot -> updateRoot x
updateRoot w =
-- w is existing root and is guaranteed to map to something
let a0 = ILM.partialLookup w fwd
in if a0 == a1
-- the value has not changed, don't need to change assoc
then ((w, AssocInsertResUnchanged), assoc)
else
-- value has changed, need to check if it's fresh
case HashMap.lookup a1 bwd of
-- never seen; insert and return
Nothing ->
let fwd' = ILM.insert w a1 fwd
bwd' = HashMap.insert a1 w (HashMap.delete a0 bwd)
in ((w, AssocInsertResUpdated), Assoc fwd' bwd' equiv)
-- mapped to another set of nodes, merge
Just v ->
let (toKeep, toDelete, equiv') = efUnsafeMerge w v equiv
res = AssocInsertResMerged toDelete
in if toKeep == w
-- w wins
then
let fwd' = ILM.insert w a1 (ILM.delete v fwd)
bwd' = HashMap.insert a1 w (HashMap.delete a0 bwd)
in ((w, res), Assoc fwd' bwd' equiv')
-- v wins
else
let fwd' = ILM.delete w fwd
bwd' = HashMap.delete a0 bwd
in ((v, res), Assoc fwd' bwd' equiv')
insertRoot w equiv' =
-- w is new root that doesn't exist
case HashMap.lookup a1 bwd of
-- never seen; insert and return
Nothing ->
let fwd' = ILM.insert w a1 fwd
bwd' = HashMap.insert a1 w bwd
in ((w, AssocInsertResCreated), Assoc fwd' bwd' equiv')
Just v ->
let (toKeep, toDelete, equiv'') = efUnsafeMerge w v equiv'
res = AssocInsertResMerged toDelete
in if toKeep == w
-- w wins
then
let fwd' = ILM.insert w a1 (ILM.delete v fwd)
bwd' = HashMap.insert a1 w bwd
in ((w, res), Assoc fwd' bwd' equiv'')
-- v wins
else
let fwd' = ILM.delete w fwd
in ((v, res), Assoc fwd' bwd equiv'')
-- | Insert into the assoc (the state version)
assocInsert :: (Coercible x Int, Ord x, Eq a, Hashable a) => x -> a -> State (Assoc x a) (x, AssocInsertRes x)
assocInsert x a = state (assocInsertInc x a)
-- | Build an assoc from a list of pairs
assocFromList :: (Coercible x Int, Ord x, Eq a, Hashable a) => [(x, a)] -> Assoc x a
assocFromList = foldl' (\assoc (x, a) -> snd (assocInsertInc x a assoc)) assocNew
-- | Turn an assoc into a list of pairs (NOTE - emits only root keys!)
assocToList :: Coercible x Int => Assoc x a -> [(x, a)]
assocToList = ILM.toList . assocFwd
-- | Is the given key in the assoc?
assocMember :: Coercible x Int => x -> Assoc x a -> Bool
assocMember x (Assoc _ _ equiv) = efMember x equiv
-- | Forward lookup
assocLookupByKey :: Coercible x Int => x -> Assoc x a -> Maybe a
assocLookupByKey x (Assoc fwd _ equiv) = ILM.lookup (efLookupRoot x equiv) fwd
-- | PARTIAL forward lookup
assocPartialLookupByKey :: Coercible x Int => x -> Assoc x a -> a
assocPartialLookupByKey x = fromJust . assocLookupByKey x
-- | Backward lookup
assocLookupByValue :: (Eq a, Hashable a) => a -> Assoc x a -> Maybe x
assocLookupByValue a = HashMap.lookup a . assocBwd
-- | PARTIAL backward lookup
assocPartialLookupByValue :: (Eq a, Hashable a) => a -> Assoc x a -> x
assocPartialLookupByValue a = flip (HashMap.!) a . assocBwd
-- | Finds the root for the given key (id if not found)
assocLookupRoot :: Coercible x Int => x -> Assoc x a -> x
assocLookupRoot x = efLookupRoot x . assocEquiv
-- | List all root (live, non-compactible) keys
assocRoots :: Coercible x Int => Assoc x a -> [x]
assocRoots = efRoots . assocEquiv
-- | List all leaf (dead, compactible) keys
assocLeaves :: Coercible x Int => Assoc x a -> [x]
assocLeaves = efLeaves . assocEquiv
-- | List all entries (root and leaf)
assocMembers :: Coercible x Int => Assoc x a -> [x]
assocMembers = efMembers . assocEquiv
-- | Are there dead keys in the equiv from 'assocInsert'?
assocCanCompact :: Assoc x a -> Bool
assocCanCompact = efCanCompact . assocEquiv
-- | Removes all dead keys in the equiv (raw version).
assocCompactInc :: Coercible x Int => Assoc x a -> (IntLikeMap x x, Assoc x a)
assocCompactInc assoc@(Assoc fwd bwd equiv) =
let replacements = efBwd equiv
assoc' =
if ILM.null replacements
then assoc
else let (_, equiv') = efCompactInc equiv in Assoc fwd bwd equiv'
in (replacements, assoc')
-- | Removes all dead keys in the equiv (state version).
-- Returns map of dead leaf node -> live root node
assocCompact :: Coercible x Int => State (Assoc x a) (IntLikeMap x x)
assocCompact = state assocCompactInc
-- | Removes the given keys from the assoc (raw version)
assocRemoveAllInc :: (Coercible x Int, Eq a, Hashable a) => [x] -> Assoc x a -> Assoc x a
assocRemoveAllInc xs (Assoc fwd0 bwd0 equiv0) = Assoc fwdFinal bwdFinal equivFinal where
(remap, equivFinal) = efRemoveAllInc xs equiv0
(fwdFinal, bwdFinal) = foldl' go (fwd0, bwd0) xs
go tup@(fwd, bwd) x =
case ILM.lookup x fwd of
-- Leaf, ignore
Nothing -> tup
-- Root
Just a ->
case ILM.lookup x remap of
-- Singleton root, delete
Nothing ->
let fwd' = ILM.delete x fwd
bwd' = HashMap.delete a bwd
in (fwd', bwd')
-- Remapped root, rotate
Just y ->
let fwd' = ILM.delete x (ILM.insert y a fwd)
bwd' = HashMap.insert a y bwd
in (fwd', bwd')
-- | Removes the given keys from the assoc (state version).
-- Values will only be removed from the assoc if the key is a singleton root.
-- If a key is not found, it is simply ignored.
assocRemoveAll :: (Coercible x Int, Eq a, Hashable a) => [x] -> State (Assoc x a) ()
assocRemoveAll = modify' . assocRemoveAllInc
-- | Join two assocs (uses the first as the base)
assocUnion :: (Coercible x Int, Ord x, Eq a, Hashable a) => Assoc x a -> Assoc x a -> Assoc x a
assocUnion base (Assoc fwd _ equiv) = Assoc fwdFinal bwdFinal equivFinal where
goRoots assocGo (x, a) = snd (assocInsertInc x a assocGo)
goLeaves equivGo (leaf, oldRoot) = efUnsafeAddLeafInc oldRoot leaf equivGo
Assoc fwdFinal bwdFinal equivMid = foldl' goRoots base (ILM.toList fwd)
equivFinal = foldl' goLeaves equivMid (ILM.toList (efBwd equiv))
-- | Returns the footprint of the given value - all keys that map to it (root and leaf)
assocFootprint :: (Coercible x Int, Eq a, Hashable a) => a -> Assoc x a -> IntLikeSet x
assocFootprint a (Assoc _ bwd equiv) =
case HashMap.lookup a bwd of
Nothing -> ILS.empty
Just r -> efEquivs r equiv