large-anon-0.3.0: src/Data/Record/Anon/Internal/Core/Diff.hs
{-# LANGUAGE CPP #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE PolyKinds #-}
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE RoleAnnotations #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE StandaloneDeriving #-}
-- | Record diff
--
-- Intended for qualified import.
--
-- > import Data.Record.Anonymous.Internal.Diff (Diff)
-- > import qualified Data.Record.Anonymous.Internal.Diff as Diff
module Data.Record.Anon.Internal.Core.Diff (
Diff(..)
-- * Incremental construction
, empty
, get
, set
, insert
-- * Batch operations
, apply
-- * Debugging support
#if DEBUG
, toString
#endif
) where
import Data.IntMap (IntMap)
import Data.Kind
import Data.List.NonEmpty (NonEmpty(..), (<|))
import Data.SOP.BasicFunctors
import GHC.Exts (Any)
import qualified Data.IntMap.Strict as IntMap
#if DEBUG
import Debug.RecoverRTTI (AnythingToString(..))
import Data.Record.Generic.Rep.Internal (noInlineUnsafeCo)
#endif
import qualified Data.List.NonEmpty as NE
import Data.Record.Anon.Internal.Core.Canonical (Canonical)
import Data.Record.Anon.Internal.Core.FieldName (FieldName)
import Data.Record.Anon.Internal.Util.SmallHashMap (SmallHashMap)
import qualified Data.Record.Anon.Internal.Core.Canonical as Canon
import qualified Data.Record.Anon.Internal.Util.SmallHashMap as HashMap
{-------------------------------------------------------------------------------
Definition
-------------------------------------------------------------------------------}
-- | Record changes to a ('Canonical') record.
--
-- Unlike 'Canon.set' and 'Canon.insert', 'Diff.set' and 'Diff.insert' deal with
-- a single field at a time, at @O(1)@ cost. This is the raison d'être of
-- 'Diff': amortize the cost of repeated updates/inserts. Specifically, a series
-- of inserts or updates will build a 'Diff' which will take @O(n)@ to apply,
-- but that 'apply' should be /executed/ only when we do an operation which is
-- @O(n)@ anyway, thereby absorbing the cost.
--
-- This is also the reason that 'Diff' is name based, not index based: inserting
-- a new field would increase all indices of existing fields by 1, an inherently
-- @O(n)@ operation.
--
-- NOTE: As for 'Canonical', when citing algorithmic complexity of operations on
-- 'Diff', we assume that 'HashMap' inserts and lookups are @O(1)@. See
-- 'Canonical' for more detailed justification.
--
-- NOTE: Since @large-anon@ currently only supports records with strict fields,
-- we use strict 'HashMap' here.
data Diff (f :: k -> Type) = Diff {
-- | New values of existing fields
--
-- Indices refer to the /canonical/ record. Since new fields are inserted
-- /after/ old fields, field indices do not change as we insert new
-- fields. This is key to the soundness of having a 'Canonical' and 'Diff'
-- pair.
diffUpd :: !(IntMap (f Any))
-- | List of new fields, most recently inserted first
--
-- May contain duplicates: fields inserted later shadow earlier fields.
, diffIns :: [FieldName]
-- | Values for the newly inserted fields
--
-- If the field is shadowed, the list will have multiple entries. Entries
-- in the lists are from new to old, so the head of the list is the
-- "currently visible" entry.
, diffNew :: !(SmallHashMap FieldName (NonEmpty (f Any)))
}
type role Diff representational
deriving instance Show a => Show (Diff (K a))
{-------------------------------------------------------------------------------
Incremental construction
The post-conditions are verified (albeit somewhat implicitly) in
"Test.Prop.Record.Diff".
-------------------------------------------------------------------------------}
-- | Empty difference
--
-- Postcondition:
--
-- > apply empty c == c
empty :: Diff f
empty = Diff {
diffUpd = IntMap.empty
, diffIns = []
, diffNew = HashMap.empty
}
-- | Get field
--
-- Precondition: field must be present in the diff or in the record.
-- Postcondition:
--
-- > Diff.get f d c == Canon.get f (Diff.apply d c)
--
-- @O(1)@.
get :: (Int, FieldName) -> Diff f -> Canonical f -> f Any
get (i, f) Diff{..} c =
case HashMap.lookup f diffNew of
Just xs -> NE.head xs -- inserted in the diff
Nothing -> case IntMap.lookup i diffUpd of
Just x -> x -- updated in the diff
Nothing -> Canon.getAtIndex c i -- unchanged in the diff
-- | Update existing field
--
-- Precondition: field must be present in the diff or in the record.
-- Postcondition:
--
-- > Diff.apply (Diff.set f x c d) c == Canon.set [(f, x)] (apply d c)
--
-- It is useful to spell out what happens when inserts and updated are mixed:
--
-- * When a field is inserted and then updated, we just update the corresponding
-- entry in 'diffNew'.
-- * When an /existing/ field is first updated and then a new field with the
-- same name is added, an entry is added to 'diffNew' but 'diffUpd' will also
-- contain an entry for this field. This doesn't matter: when the diff is
-- applied, the new field will shadow the old, and when we 'get' the value
-- of a field, we similarly /first/ check 'diffNew'.
-- * When the /same/ field is inserted more than once, updates to that field
-- will effectively affect all of them (since we store only a single value),
-- but only the first value will matter as it will shadow all the others.
--
-- @O(1)@.
set :: forall f. (Int, FieldName) -> f Any -> Diff f -> Diff f
set (i, f) x d@Diff{..} =
case HashMap.alterExisting f updateInserted diffNew of
Just ((), diffNew') -> d { diffNew = diffNew' }
Nothing -> d { diffUpd = IntMap.insert i x diffUpd }
where
updateInserted :: NonEmpty (f Any) -> ((), Maybe (NonEmpty (f Any)))
updateInserted (_ :| prev) = ((), Just (x :| prev))
-- | Insert new field
--
-- Precondition: none (if the field already exists, it will be shadowed).
-- Postcondition:
--
-- > Diff.apply (Diff.insert f x d) c = Canon.insert [(f, x)] (apply d c)
--
-- @(1)@.
insert :: forall f. FieldName -> f Any -> Diff f -> Diff f
insert f x d@Diff{..} = d {
diffIns = f : diffIns
, diffNew = HashMap.alter (Just . insertField) f diffNew
}
where
insertField :: Maybe (NonEmpty (f Any)) -> NonEmpty (f Any)
insertField Nothing = x :| []
insertField (Just prev) = x <| prev
{-------------------------------------------------------------------------------
Batch operations
-------------------------------------------------------------------------------}
-- | All new fields (including shadowed fields), from new to old
--
-- @O(n)@.
allNewFields :: Diff f -> [f Any]
allNewFields = \Diff{..} -> go diffNew diffIns
where
go :: SmallHashMap FieldName (NonEmpty (f Any)) -> [FieldName] -> [f Any]
go _ [] = []
go vs (x:xs) = case HashMap.alterExisting x NE.uncons vs of
Nothing -> error "allNewFields: invariant violation"
Just (v, vs') -> v : go vs' xs
-- | Apply diff
--
-- @O(n)@ in the size of the 'Canonical' and the 'Diff' in general.
-- @O(1)@ if the `Diff` is empty.
apply :: forall f. Diff f -> Canonical f -> Canonical f
apply d =
Canon.insert (allNewFields d)
. Canon.setAtIndex (IntMap.toList (diffUpd d))
{-------------------------------------------------------------------------------
Debugging support
-------------------------------------------------------------------------------}
#if DEBUG
toString :: forall k (f :: k -> Type). Diff f -> String
toString = show . mapDiff (K . AnythingToString . co)
where
mapDiff :: (forall x. f x -> g x) -> Diff f -> Diff g
mapDiff f Diff{..} = Diff{
diffUpd = fmap f diffUpd
, diffIns = diffIns
, diffNew = fmap (fmap f) diffNew
}
co :: f x -> f Any
co = noInlineUnsafeCo
#endif