packages feed

unimap-0.1.0: src/Unimap.hs

-- | (Import this module qualified)
module Unimap
  ( Changed (..)
  , maybeChanged
  , Equiv (..)
  , emptyEquiv
  , Entry (..)
  , filterRootEntries
  , MergeOne
  , MergeMany
  , adaptMergeOne
  , foldMergeOne
  , foldMergeMany
  , concatMergeOne
  , concatMergeMany
  , UnionMap (unUnionMap)
  , UnionMapLens
  , empty
  , size
  , member
  , toList
  , values
  , AddRes (..)
  , AddVal (..)
  , add
  , addLM
  , addM
  , TraceRes (..)
  , trace
  , LookupRes (..)
  , LookupVal (..)
  , lookup
  , lookupLM
  , lookupM
  , equiv
  , equivLM
  , equivM
  , compact
  , compactLM
  , compactM
  , canonicalize
  , canonicalizeLM
  , canonicalizeM
  , UpdateRes (..)
  , UpdateVal (..)
  , update
  , updateLM
  , updateM
  , MergeRes (..)
  , MergeVal (..)
  , mergeOne
  , mergeOneLM
  , mergeOneM
  , mergeMany
  , mergeManyLM
  , mergeManyM
  , extract
  , extractLM
  , extractM
  )
where

import Control.Monad.Except (Except, MonadError (..), runExcept)
import Control.Monad.State.Strict (MonadState, StateT, get, put, runStateT, state)
import Data.Coerce (Coercible)
import Data.Foldable (fold, foldl')
import Data.Foldable qualified as F
import Data.Maybe (fromMaybe, mapMaybe)
import IntLike.Map (IntLikeMap)
import IntLike.Map qualified as ILM
import IntLike.Set (IntLikeSet)
import IntLike.Set qualified as ILS
import Optics (Lens', Traversal', equality', over, set, view)
import Prelude hiding (lookup)

-- Private util

mayStateLens :: (MonadState s m) => Lens' s x -> (x -> (a, Maybe x)) -> m a
mayStateLens l f = state $ \s ->
  let u = view l s
      (a, mu') = f u
  in  case mu' of
        Nothing -> (a, s)
        Just u' ->
          let s' = set l u' s
          in  (a, s')

stateLens :: (MonadState s m) => Lens' s x -> (x -> (a, x)) -> m a
stateLens l f = state $ \s ->
  let u = view l s
      (a, u') = f u
      s' = set l u' s
  in  (a, s')

newtype DropM e s a = DropM {unDropM :: StateT s (Except e) a}
  deriving newtype (Functor, Applicative, Monad, MonadState s, MonadError e)

runDropM :: DropM e s a -> s -> Either e (a, s)
runDropM it s = runExcept (runStateT (unDropM it) s)

safeTail :: [a] -> [a]
safeTail xs =
  case xs of
    [] -> xs
    _ : ys -> ys

-- The real stuff

data Changed
  = ChangedYes
  | ChangedNo
  deriving stock (Eq, Show)

maybeChanged :: Maybe a -> Changed
maybeChanged = \case
  Nothing -> ChangedNo
  Just _ -> ChangedYes

data Equiv k = Equiv
  { equivFwd :: !(IntLikeMap k (IntLikeSet k))
  , equivBwd :: !(IntLikeMap k k)
  }
  deriving stock (Eq, Show)

emptyEquiv :: Equiv k
emptyEquiv = Equiv ILM.empty ILM.empty

data Entry k v
  = EntryLink !k
  | EntryValue !v
  deriving stock (Eq, Show, Ord, Functor, Foldable, Traversable)

filterRootEntries :: (Coercible k Int) => IntLikeMap k (Entry k v) -> IntLikeMap k v
filterRootEntries = ILM.fromList . mapMaybe (uncurry go) . ILM.toList
 where
  go k = \case
    EntryLink _ -> Nothing
    EntryValue v -> Just (k, v)

type MergeOne e v r = Maybe v -> v -> Either e (r, v)

type MergeMany f e v r = Maybe v -> f v -> Either e (r, v)

adaptMergeOne :: (v -> f v) -> MergeMany f e v r -> MergeOne e v r
adaptMergeOne h g mv = g mv . h

foldMergeOne :: (Monoid r) => (v -> v -> Either e (r, v)) -> MergeOne e v r
foldMergeOne g mv v =
  case mv of
    Nothing -> Right (mempty, v)
    Just w -> g w v

foldMergeMany :: (Foldable f, Monoid r) => Either e v -> (v -> v -> Either e (r, v)) -> MergeMany f e v r
foldMergeMany onE g mv fv = start
 where
  start =
    let vs = F.toList fv
    in  case maybe vs (: vs) mv of
          [] -> fmap (mempty,) onE
          y : ys -> go mempty y ys
  go !r !v = \case
    [] -> Right (r, v)
    w : ws ->
      case g v w of
        Right (s, u) -> go (r <> s) u ws
        e@(Left _) -> e

concatMergeOne :: (Semigroup v) => MergeOne e v ()
concatMergeOne mv v = Right ((), maybe v (<> v) mv)

concatMergeMany :: (Foldable f, Monoid v) => MergeMany f e v ()
concatMergeMany mv vs = concatMergeOne mv (fold vs)

newtype UnionMap k v = UnionMap {unUnionMap :: IntLikeMap k (Entry k v)}
  deriving stock (Eq, Show, Functor, Foldable, Traversable)

type UnionMapLens s k v = Lens' s (UnionMap k v)

empty :: UnionMap k v
empty = UnionMap ILM.empty

size :: UnionMap k v -> Int
size = ILM.size . unUnionMap

member :: (Coercible k Int) => k -> UnionMap k v -> Bool
member k = ILM.member k . unUnionMap

toList :: (Coercible k Int) => UnionMap k v -> [(k, Entry k v)]
toList = ILM.toList . unUnionMap

values :: (Coercible k Int) => UnionMap k v -> IntLikeMap k v
values = foldl' go ILM.empty . toList
 where
  go m (k, ue) =
    case ue of
      EntryValue v -> ILM.insert k v m
      _ -> m

data AddRes k v
  = AddResAdded !(UnionMap k v)
  | AddResDuplicate
  deriving stock (Eq, Show)

add :: (Coercible k Int) => k -> v -> UnionMap k v -> AddRes k v
add k v (UnionMap m) =
  case ILM.lookup k m of
    Nothing -> AddResAdded (UnionMap (ILM.insert k (EntryValue v) m))
    Just _ -> AddResDuplicate

data AddVal
  = AddValAdded
  | AddValDuplicate
  deriving stock (Eq, Show)

addLM :: (Coercible k Int, MonadState s m) => UnionMapLens s k v -> k -> v -> m AddVal
addLM l k v = mayStateLens l $ \u ->
  case add k v u of
    AddResAdded w -> (AddValAdded, Just w)
    AddResDuplicate -> (AddValDuplicate, Nothing)

addM :: (Coercible k Int, MonadState (UnionMap k v) m) => k -> v -> m AddVal
addM = addLM equality'

data TraceRes k v
  = TraceResMissing !k
  | TraceResFound !k !v ![k]
  deriving stock (Eq, Show)

trace :: (Coercible k Int) => k -> UnionMap k v -> TraceRes k v
trace k (UnionMap m) = go [] k
 where
  go !acc j =
    case ILM.lookup j m of
      Nothing -> TraceResMissing j
      Just link -> case link of
        EntryLink kx -> go (j : acc) kx
        EntryValue v -> TraceResFound j v acc

data LookupRes k v
  = LookupResMissing !k
  | LookupResFound !k !v !(Maybe (UnionMap k v))
  deriving stock (Eq, Show)

lookup :: (Coercible k Int) => k -> UnionMap k v -> LookupRes k v
lookup k u = case trace k u of
  TraceResMissing kx -> LookupResMissing kx
  TraceResFound kr vr acc ->
    let mu =
          if null acc
            then Nothing
            else Just (foldl' (\(UnionMap n) kx -> UnionMap (ILM.insert kx (EntryLink kr) n)) u (safeTail acc))
    in  LookupResFound kr vr mu

data LookupVal k v
  = LookupValMissing !k
  | LookupValOk !k !v !Changed
  deriving stock (Eq, Show)

lookupLM :: (Coercible k Int, MonadState s m) => UnionMapLens s k v -> k -> m (LookupVal k v)
lookupLM l k = mayStateLens l $ \u ->
  case lookup k u of
    LookupResMissing x -> (LookupValMissing x, Nothing)
    LookupResFound x y mw -> (LookupValOk x y (maybeChanged mw), mw)

lookupM :: (Coercible k Int, MonadState (UnionMap k v) m) => k -> m (LookupVal k v)
lookupM = lookupLM equality'

equiv :: (Coercible k Int) => UnionMap k v -> (Equiv k, Maybe (UnionMap k v))
equiv u = foldl' go (emptyEquiv, Nothing) (toList u)
 where
  go (Equiv fwd bwd, mw) (k, ue) =
    case ue of
      EntryValue _ ->
        let fwd' = ILM.alter (Just . fromMaybe ILS.empty) k fwd
        in  (Equiv fwd' bwd, mw)
      EntryLink _ ->
        case lookup k (fromMaybe u mw) of
          LookupResMissing _ -> error "impossible"
          LookupResFound r _ mw' ->
            let fwd' = ILM.alter (Just . maybe (ILS.singleton k) (ILS.insert k)) r fwd
                bwd' = ILM.insert k r bwd
            in  (Equiv fwd' bwd', mw')

equivLM :: (Coercible k Int, MonadState s m) => UnionMapLens s k v -> m (Equiv k)
equivLM l = mayStateLens l equiv

equivM :: (Coercible k Int, MonadState (UnionMap k v) m) => m (Equiv k)
equivM = equivLM equality'

-- | Compresses all paths so there is never more than one jump to the root of each class
-- Retains all keys in the map but returns a mapping of non-root -> root keys
compact :: (Coercible k Int) => UnionMap k v -> (IntLikeMap k k, UnionMap k v)
compact u = foldl' go (ILM.empty, u) (toList u)
 where
  go mw@(m, w) (k, ue) =
    if ILM.member k m
      then mw
      else case ue of
        EntryValue _ -> mw
        EntryLink _ ->
          case trace k w of
            TraceResMissing _ -> error "impossible"
            TraceResFound r _ kacc ->
              foldl' (\(m', w') j -> (ILM.insert j r m', UnionMap (ILM.insert j (EntryLink r) (unUnionMap w')))) mw kacc

compactLM :: (Coercible k Int, MonadState s m) => UnionMapLens s k v -> m (IntLikeMap k k)
compactLM l = stateLens l compact

compactM :: (Coercible k Int, MonadState (UnionMap k v) m) => m (IntLikeMap k k)
compactM = compactLM equality'

-- | Compacts and rewrites all values with canonical keys.
-- Retains all keys in the map and again returns a mapping of non-root -> root keys.
-- TODO remove non-canonical keys?
canonicalize :: (Coercible k Int) => Traversal' v k -> UnionMap k v -> (IntLikeMap k k, UnionMap k v)
canonicalize t u = res
 where
  res = let (m, UnionMap w) = compact u in (m, UnionMap (fmap (go m) w))
  go m ue =
    case ue of
      EntryLink _ -> ue
      EntryValue fk -> EntryValue (over t (\j -> ILM.findWithDefault j j m) fk)

canonicalizeLM
  :: (Coercible k Int, MonadState s m) => UnionMapLens s k v -> Traversal' v k -> m (IntLikeMap k k)
canonicalizeLM l t = stateLens l (canonicalize t)

canonicalizeM :: (Coercible k Int, MonadState (UnionMap k v) m) => Traversal' v k -> m (IntLikeMap k k)
canonicalizeM = canonicalizeLM equality'

data UpdateRes e k v r
  = UpdateResMissing !k
  | UpdateResEmbed !e
  | UpdateResAdded !v !r !(UnionMap k v)
  | UpdateResUpdated !k !v !r !(UnionMap k v)
  deriving stock (Eq, Show)

update :: (Coercible k Int, Eq k) => MergeOne e v r -> k -> v -> UnionMap k v -> UpdateRes e k v r
update g k v u@(UnionMap m) = goLookupK
 where
  goLookupK = case lookup k u of
    LookupResMissing kx ->
      if k == kx
        then goAdd
        else UpdateResMissing kx
    LookupResFound kr vr mu -> goMerge kr vr (fromMaybe u mu)
  goAdd =
    case g Nothing v of
      Left e -> UpdateResEmbed e
      Right (r, vg) -> UpdateResAdded vg r (UnionMap (ILM.insert k (EntryValue v) m))
  goMerge kr vr (UnionMap mr) =
    case g (Just vr) v of
      Left e -> UpdateResEmbed e
      Right (r, vg) -> UpdateResUpdated kr vg r (UnionMap (ILM.insert kr (EntryValue vg) mr))

data UpdateVal e k v r
  = UpdateValMissing !k
  | UpdateValEmbed !e
  | UpdateValAdded !v !r
  | UpdateValUpdated !k !v !r
  deriving stock (Eq, Show)

updateLM
  :: (Coercible k Int, Eq k, MonadState s m) => UnionMapLens s k v -> MergeOne e v r -> k -> v -> m (UpdateVal e k v r)
updateLM l g k v = mayStateLens l $ \u ->
  case update g k v u of
    UpdateResMissing x -> (UpdateValMissing x, Nothing)
    UpdateResEmbed e -> (UpdateValEmbed e, Nothing)
    UpdateResAdded y r w -> (UpdateValAdded y r, Just w)
    UpdateResUpdated x y r w -> (UpdateValUpdated x y r, Just w)

updateM :: (Coercible k Int, Eq k, MonadState (UnionMap k v) m) => MergeOne e v r -> k -> v -> m (UpdateVal e k v r)
updateM = updateLM equality'

data MergeRes e k v r
  = MergeResMissing !k
  | MergeResEmbed !e
  | MergeResMerged !k !v !r !(UnionMap k v)
  deriving stock (Eq, Show)

mergeOne :: (Coercible k Int, Eq k) => MergeOne e v r -> k -> k -> UnionMap k v -> MergeRes e k v r
mergeOne g k j u = goLookupK
 where
  doCompactCheck kr jr w jacc = doCompact kr w (if kr == jr then safeTail jacc else jr : jacc)
  doCompact kr w acc = UnionMap (foldl' (\m x -> ILM.insert x (EntryLink kr) m) (unUnionMap w) acc)
  doRoot kr gv w = UnionMap (ILM.insert kr (EntryValue gv) (unUnionMap w))
  goLookupK = case lookup k u of
    LookupResMissing kx -> if k == kx then goAssign else MergeResMissing kx
    LookupResFound kr kv mw -> goLookupJ kr kv (fromMaybe u mw)
  goAssign = case trace j u of
    TraceResMissing jx -> MergeResMissing jx
    TraceResFound jr jv jacc -> goMerge k Nothing jv (doCompactCheck k jr u jacc)
  goLookupJ kr kv w = case trace j w of
    TraceResMissing jx -> MergeResMissing jx
    TraceResFound jr jv jacc -> goMerge kr (Just kv) jv (doCompactCheck kr jr w jacc)
  goMerge kr mkv jv w1 =
    case g mkv jv of
      Left e -> MergeResEmbed e
      Right (r, gv) ->
        let w2 = doRoot kr gv w1
        in  MergeResMerged kr gv r w2

data MergeVal e k v r
  = MergeValMissing !k
  | MergeValEmbed !e
  | MergeValMerged !k !v !r
  deriving stock (Eq, Show)

mergeOneLM
  :: (Coercible k Int, Eq k, MonadState s m) => UnionMapLens s k v -> MergeOne e v r -> k -> k -> m (MergeVal e k v r)
mergeOneLM l g k j = mayStateLens l $ \u ->
  case mergeOne g k j u of
    MergeResMissing x -> (MergeValMissing x, Nothing)
    MergeResEmbed e -> (MergeValEmbed e, Nothing)
    MergeResMerged x y r w -> (MergeValMerged x y r, Just w)

mergeOneM :: (Coercible k Int, Eq k, MonadState (UnionMap k v) m) => MergeOne e v r -> k -> k -> m (MergeVal e k v r)
mergeOneM = mergeOneLM equality'

mergeMany :: (Traversable f, Coercible k Int, Eq k) => MergeMany f e v r -> k -> f k -> UnionMap k v -> MergeRes e k v r
mergeMany g k js u = goLookupK
 where
  doCompactCheck kr jr w jacc = doCompact kr w (if kr == jr then safeTail jacc else jr : jacc)
  doCompact kr w acc = UnionMap (foldl' (\m x -> ILM.insert x (EntryLink kr) m) (unUnionMap w) acc)
  doRoot kr gv w = UnionMap (ILM.insert kr (EntryValue gv) (unUnionMap w))
  doTraceJ kr y = do
    w <- get
    case trace y w of
      TraceResMissing jx -> throwError jx
      TraceResFound jr jv jacc -> do
        put (doCompactCheck kr jr w jacc)
        pure jv
  doTraceJs kr = runDropM (traverse (doTraceJ kr) js)
  goLookupK = case lookup k u of
    LookupResMissing kx -> if k == kx then goAssign else MergeResMissing kx
    LookupResFound kr kv mw -> goLookupJs kr kv (fromMaybe u mw)
  goAssign =
    case doTraceJs k u of
      Left jx -> MergeResMissing jx
      Right (jvs, w1) -> goMerge k Nothing jvs w1
  goLookupJs kr kv w =
    case doTraceJs kr w of
      Left jx -> MergeResMissing jx
      Right (jvs, w1) -> goMerge kr (Just kv) jvs w1
  goMerge kr mkv jvs w1 =
    case g mkv jvs of
      Left e -> MergeResEmbed e
      Right (r, gv) ->
        let w2 = doRoot kr gv w1
        in  MergeResMerged kr gv r w2

mergeManyLM
  :: (Traversable f, Coercible k Int, Eq k, MonadState s m)
  => UnionMapLens s k v
  -> MergeMany f e v r
  -> k
  -> f k
  -> m (MergeVal e k v r)
mergeManyLM l g k js = mayStateLens l $ \u ->
  case mergeMany g k js u of
    MergeResMissing x -> (MergeValMissing x, Nothing)
    MergeResEmbed e -> (MergeValEmbed e, Nothing)
    MergeResMerged x y r w -> (MergeValMerged x y r, Just w)

mergeManyM
  :: (Traversable f, Coercible k Int, Eq k, MonadState (UnionMap k v) m)
  => MergeMany f e v r
  -> k
  -> f k
  -> m (MergeVal e k v r)
mergeManyM = mergeManyLM equality'

-- | Return the subgraph accessible from a given key.
extract :: (Coercible k Int) => Traversal' v k -> k -> UnionMap k v -> (Maybe (k, IntLikeMap k v), UnionMap k v)
extract t k0 u =
  let (m, w) = canonicalize t u
      n = filterRootEntries (unUnionMap w)
      mk1 = case ILM.lookup k0 m of
        Nothing -> k0 <$ ILM.lookup k0 n
        Just k1 -> Just k1
      mp = fmap (,n) mk1
  in  (mp, w)

extractLM
  :: (Coercible k Int, MonadState s m)
  => UnionMapLens s k v
  -> Traversal' v k
  -> k
  -> m (Maybe (k, IntLikeMap k v))
extractLM l t k = stateLens l (extract t k)

extractM
  :: (Coercible k Int, MonadState (UnionMap k v) m)
  => Traversal' v k
  -> k
  -> m (Maybe (k, IntLikeMap k v))
extractM = extractLM equality'