large-anon-0.3.4: src/Data/Record/Anon/Internal/Core/Canonical.hs
{-# LANGUAGE BangPatterns #-}
{-# LANGUAGE CPP #-}
{-# LANGUAGE DerivingStrategies #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE KindSignatures #-}
{-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE PolyKinds #-}
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE StandaloneDeriving #-}
{-# LANGUAGE TypeOperators #-}
{-# LANGUAGE RoleAnnotations #-}
-- | Canonical gecord (i.e., no diff)
--
-- Intended for qualified import.
--
-- > import Data.Record.Anonymous.Internal.Canonical (Canonical)
-- > import qualified Data.Record.Anonymous.Internal.Canonical as Canon
module Data.Record.Anon.Internal.Core.Canonical (
Canonical -- opaque
-- * Indexed access
, getAtIndex
, setAtIndex
-- * Conversion
, fromRowOrderList
, toRowOrderList
, fromRowOrderArray
, toRowOrderArray
, arrayIndicesInRowOrder
-- * Basic API
, insert
, lens
-- * Simple (non-constrained) combinators
, map
, mapM
, zipWith
, zipWithM
, collapse
, sequenceA
, ap
-- * Debugging support
#if DEBUG
, toString
#endif
) where
import Prelude hiding (map, mapM, zipWith, sequenceA, pure)
import qualified Prelude
import Data.Coerce (coerce)
import Data.Kind
import Data.SOP.BasicFunctors
import Data.SOP.Classes (type (-.->)(apFn))
import GHC.Exts (Any)
#if DEBUG
import Debug.RecoverRTTI (AnythingToString(..))
#endif
import qualified Data.Foldable as Foldable
import Data.Record.Anon.Internal.Util.StrictArray (StrictArray)
import qualified Data.Record.Anon.Internal.Util.StrictArray as Strict
import Data.Primitive (SmallArray)
{-------------------------------------------------------------------------------
Definition
-------------------------------------------------------------------------------}
-- | Canonical record representation
--
-- Canonicity here refers to the fact that we have no @Diff@ to apply (see
-- "Data.Record.Anonymous.Internal.Diff").
--
-- == Order
--
-- The record is represented as a strict vector in row order (@large-anon@ is
-- strict by default; lazy records can be achieved using boxing). This order is
-- important: it makes it possible to define functions such as @mapM@ (for which
-- ordering must be well-defined).
--
-- /Indices/ into the array however are interpreted from the /end/ of the array.
-- This ensures that when we insert new elements into the record, the indices of
-- the already existing fields do not change; see 'Diff' for further discussion.
--
-- == Shadowing
--
-- Type level shadowing is reflected at the term level: if a record has
-- duplicate fields in its type, it will have multiple entries for that field
-- in the vector.
--
-- TODO: Currently we have no way of recovering the value of shadowed fields,
-- adding an API for that is future work. The work by Daan Leijen on scoped
-- labels might offer some inspiration there.
--
-- == Note on complexity
--
-- When we cite the algorithmic complexity of operations on 'Canonical', we
-- assume that 'HashMap' inserts and lookups are @O(1)@, which they are in
-- practice (especially given the relatively small size of typical records),
-- even if theoretically they are @O(log n)@. See also the documentation of
-- "Data.HashMap.Strict".
newtype Canonical (f :: k -> Type) = Canonical {
-- | To strict vector
toVector :: StrictArray Strict.ReverseIndex (f Any)
}
deriving newtype (Semigroup, Monoid)
type role Canonical representational
deriving instance Show a => Show (Canonical (K a))
{-------------------------------------------------------------------------------
Indexed access
-------------------------------------------------------------------------------}
-- | Get field at the specified index
--
-- @O(1)@.
getAtIndex :: Canonical f -> Int -> f Any
getAtIndex (Canonical c) ix = c Strict.! Strict.ReverseIndex ix
-- | Set fields at the specified indices
--
-- @O(n)@ in the size of the record (independent of the number of field updates)
-- @O(1)@ if the list of updates is empty.
setAtIndex :: [(Int, f Any)] -> Canonical f -> Canonical f
setAtIndex [] c = c
setAtIndex fs (Canonical v) = Canonical (v Strict.// co fs)
where
co :: [(Int, f Any)] -> [(Strict.ReverseIndex, f Any)]
co = coerce
{-------------------------------------------------------------------------------
Conversion
-------------------------------------------------------------------------------}
-- | From list of fields in row order
--
-- @O(n)@.
fromRowOrderList :: [f Any] -> Canonical f
fromRowOrderList = Canonical . Strict.fromList
-- | All fields in row order
--
-- @O(n)@
toRowOrderList :: Canonical f -> [f Any]
toRowOrderList = Foldable.toList . toVector
toRowOrderArray :: Canonical f -> SmallArray (f Any)
toRowOrderArray = Strict.toLazy . toVector
fromRowOrderArray :: SmallArray (f Any) -> Canonical f
fromRowOrderArray = Canonical . Strict.fromLazy
-- | Given the length of the array, all indices in row order
arrayIndicesInRowOrder :: Int -> [Int]
arrayIndicesInRowOrder 0 = []
arrayIndicesInRowOrder n = Prelude.map (Strict.arrayIndex n) [
Strict.ReverseIndex i
| i <- [0 .. pred n]
]
{-------------------------------------------------------------------------------
Basic API
-------------------------------------------------------------------------------}
-- | Insert fields into the record
--
-- It is the responsibility of the caller to make sure that the linear
-- concatenation of the new fields to the existing record matches the row order
-- of the new record.
--
-- @O(n)@ in the number of inserts and the size of the record.
-- @O(1)@ if the list of inserts is empty.
insert :: forall f. [f Any] -> Canonical f -> Canonical f
insert [] = id
insert new = prepend
where
prepend :: Canonical f -> Canonical f
prepend (Canonical v) = Canonical (Strict.fromList new <> v)
-- | Project out some fields in the selected order
--
-- It is the responsibility of the caller that the list of indices is in row
-- order of the new record.
--
-- @O(n)@ (in both directions)
lens :: [Int] -> Canonical f -> (Canonical f, Canonical f -> Canonical f)
lens is (Canonical v) = (
Canonical $
Strict.backpermute v (co is)
, \(Canonical v') -> Canonical $
Strict.update v (zip (co is) $ Foldable.toList v')
)
where
co :: [Int] -> [Strict.ReverseIndex]
co = coerce
{-------------------------------------------------------------------------------
Simple (non-constrained) combinators
-------------------------------------------------------------------------------}
map :: (forall x. f x -> g x) -> Canonical f -> Canonical g
map f (Canonical v) = Canonical $ fmap f v
mapM ::
Applicative m
=> (forall x. f x -> m (g x))
-> Canonical f -> m (Canonical g)
mapM f (Canonical v) = Canonical <$> Strict.mapM f v
-- | Zip two records
--
-- Precondition: the two records must have the same shape.
zipWith ::
(forall x. f x -> g x -> h x)
-> Canonical f -> Canonical g -> Canonical h
zipWith f (Canonical v) (Canonical v') = Canonical $ Strict.zipWith f v v'
-- | Applicative zip of two records
--
-- Precondition: the two records must have the same shape.
zipWithM ::
Applicative m
=> (forall x. f x -> g x -> m (h x))
-> Canonical f -> Canonical g -> m (Canonical h)
zipWithM f (Canonical v) (Canonical v') = Canonical <$> Strict.zipWithM f v v'
collapse :: Canonical (K a) -> [a]
collapse (Canonical v) = co $ Foldable.toList v
where
co :: [K a Any] -> [a]
co = coerce
sequenceA :: Applicative m => Canonical (m :.: f) -> m (Canonical f)
sequenceA (Canonical v) = Canonical <$> Strict.mapM unComp v
ap :: Canonical (f -.-> g) -> Canonical f -> Canonical g
ap = zipWith apFn
{-------------------------------------------------------------------------------
Debugging support
-------------------------------------------------------------------------------}
#if DEBUG
toString :: forall k (f :: k -> Type). Canonical f -> String
toString = show . aux
where
aux :: Canonical f -> Canonical (K (AnythingToString (f Any)))
aux = coerce
#endif