cryptol-3.1.0: src/Cryptol/Utils/RecordMap.hs
-- |
-- Module : Cryptol.Utils.RecordMap
-- Copyright : (c) 2020 Galois, Inc.
-- License : BSD3
-- Maintainer : cryptol@galois.com
-- Stability : provisional
-- Portability : portable
--
-- This module implements an "order insensitive" datastructure for
-- record types and values. For most purposes, we want to deal with
-- record fields in a canonical order; but for user interaction
-- purposes, we generally want to display the fields in the order they
-- were specified by the user (in source files, at the REPL, etc.).
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE Safe #-}
module Cryptol.Utils.RecordMap
( RecordMap
, displayOrder
, canonicalFields
, displayFields
, recordElements
, displayElements
, fieldSet
, recordFromFields
, recordFromFieldsErr
, recordFromFieldsWithDisplay
, lookupField
, adjustField
, traverseRecordMap
, mapWithFieldName
, zipRecordsM
, zipRecords
, recordMapAccum
) where
import Control.DeepSeq
import Control.Monad.Except (ExceptT, MonadError(..), runExceptT)
import Control.Monad.Trans (MonadTrans(..))
import Data.Functor.Identity
import Data.Set (Set)
import Data.Map (Map)
import qualified Data.Map.Strict as Map
import qualified Data.Map.Merge.Strict as Map
import Cryptol.Utils.Panic
-- | An "order insensitive" datastructure.
-- The fields can be accessed either according
-- to a "canonical" order, or based on a
-- "display" order, which matches the order
-- in which the fields were originally specified.
data RecordMap a b =
RecordMap
{ recordMap :: !(Map a b)
, _displayOrder :: [a]
}
-- RecordMap Invariant:
-- The `displayOrder` field should contain exactly the
-- same set of field names as the keys of `recordMap`.
-- Moreover, each field name should occur at most once.
instance (Ord a, Eq b) => Eq (RecordMap a b) where
a == b = recordMap a == recordMap b
instance (Ord a, Ord b) => Ord (RecordMap a b) where
compare a b = compare (recordMap a) (recordMap b)
instance (Show a, Ord a, Show b) => Show (RecordMap a b) where
show = show . displayFields
instance (NFData a, NFData b) => NFData (RecordMap a b) where
rnf = rnf . canonicalFields
-- | Return the fields in this record as a set.
fieldSet :: Ord a => RecordMap a b -> Set a
fieldSet r = Map.keysSet (recordMap r)
-- | The order in which the fields originally appeared.
displayOrder :: RecordMap a b -> [a]
displayOrder r = _displayOrder r
-- | Retrieve the elements of the record in canonical order
-- of the field names
recordElements :: RecordMap a b -> [b]
recordElements = map snd . canonicalFields
-- | Return a list of field/value pairs in canonical order.
canonicalFields :: RecordMap a b -> [(a,b)]
canonicalFields = Map.toList . recordMap
-- | Retrieve the elements of the record in display order
-- of the field names.
displayElements :: (Show a, Ord a) => RecordMap a b -> [b]
displayElements = map snd . displayFields
-- | Return a list of field/value pairs in display order.
displayFields :: (Show a, Ord a) => RecordMap a b -> [(a,b)]
displayFields r = map find (displayOrder r)
where
find x =
case Map.lookup x (recordMap r) of
Just v -> (x, v)
Nothing ->
panic "displayFields"
["Could not find field in recordMap " ++ show x
, "Display order: " ++ show (displayOrder r)
, "Canonical order:" ++ show (map fst (canonicalFields r))
]
-- | Generate a record from a list of field/value pairs.
-- Precondition: each field identifier appears at most
-- once in the given list.
recordFromFields :: (Show a, Ord a) => [(a,b)] -> RecordMap a b
recordFromFields xs =
case recordFromFieldsErr xs of
Left (x,_) ->
panic "recordFromFields"
["Repeated field value: " ++ show x]
Right r -> r
-- | Generate a record from a list of field/value pairs.
-- If any field name is repeated, the first repeated name/value
-- pair is returned. Otherwise the constructed record is returned.
recordFromFieldsErr :: (Show a, Ord a) => [(a,b)] -> Either (a,b) (RecordMap a b)
recordFromFieldsErr xs0 = loop mempty xs0
where
loop m [] = Right (RecordMap m (map fst xs0))
loop m ((x,v):xs)
| Just _ <- Map.lookup x m = Left (x,v)
| otherwise = loop (Map.insert x v m) xs
-- | Generate a record from a list of field/value pairs,
-- and also provide the "display" order for the fields directly.
-- Precondition: each field identifier appears at most once in each
-- list, and a field name appears in the display order iff it appears
-- in the field list.
recordFromFieldsWithDisplay :: (Show a, Ord a) => [a] -> [(a,b)] -> RecordMap a b
recordFromFieldsWithDisplay dOrd fs = r { _displayOrder = dOrd }
where
r = recordFromFields fs
-- | Lookup the value of a field
lookupField :: Ord a => a -> RecordMap a b -> Maybe b
lookupField x m = Map.lookup x (recordMap m)
-- | Update the value of a field by applying the given function.
-- If the field is not present in the record, return Nothing.
adjustField :: forall a b. Ord a => a -> (b -> b) -> RecordMap a b -> Maybe (RecordMap a b)
adjustField x f r = mkRec <$> Map.alterF f' x (recordMap r)
where
mkRec m = r{ recordMap = m }
f' :: Maybe b -> Maybe (Maybe b)
f' Nothing = Nothing
f' (Just v) = Just (Just (f v))
-- | Traverse the elements of the given record map in canonical
-- order, applying the given action.
traverseRecordMap :: Applicative t =>
(a -> b -> t c) -> RecordMap a b -> t (RecordMap a c)
traverseRecordMap f r =
(\m -> RecordMap m (displayOrder r)) <$> Map.traverseWithKey f (recordMap r)
-- | Apply the given function to each element of a record.
mapWithFieldName :: (a -> b -> c) -> RecordMap a b -> RecordMap a c
mapWithFieldName f = runIdentity . traverseRecordMap (\a b -> Identity (f a b))
instance Functor (RecordMap a) where
fmap f = mapWithFieldName (\_ -> f)
instance Foldable (RecordMap a) where
foldMap f = foldMap (f . snd) . canonicalFields
instance Traversable (RecordMap a) where
traverse f = traverseRecordMap (\_ -> f)
-- | The function recordMapAccum threads an accumulating argument through
-- the map in canonical order of fields.
recordMapAccum :: (a -> b -> (a,c)) -> a -> RecordMap k b -> (a, RecordMap k c)
recordMapAccum f z r = (a, r{ recordMap = m' })
where
(a, m') = Map.mapAccum f z (recordMap r)
-- | Zip together the fields of two records using the provided action.
-- If some field is present in one record, but not the other,
-- an @Either a a@ will be returned, indicating which record is missing
-- the field, and returning the name of the missing field.
--
-- The @displayOrder@ of the resulting record will be taken from the first
-- argument (rather arbitrarily).
zipRecordsM :: forall t a b c d. (Ord a, Monad t) =>
(a -> b -> c -> t d) -> RecordMap a b -> RecordMap a c -> t (Either (Either a a) (RecordMap a d))
zipRecordsM f r1 r2 = runExceptT (mkRec <$> zipMap (recordMap r1) (recordMap r2))
where
mkRec m = RecordMap m (displayOrder r1)
zipMap :: Map a b -> Map a c -> ExceptT (Either a a) t (Map a d)
zipMap = Map.mergeA missingLeft missingRight matched
missingLeft = Map.traverseMissing (\a _b -> throwError (Left a))
missingRight = Map.traverseMissing (\a _c -> throwError (Right a))
matched = Map.zipWithAMatched (\a b c -> lift (f a b c))
-- | Pure version of `zipRecordsM`
zipRecords :: forall a b c d. Ord a =>
(a -> b -> c -> d) -> RecordMap a b -> RecordMap a c -> Either (Either a a) (RecordMap a d)
zipRecords f r1 r2 = runIdentity (zipRecordsM (\a b c -> Identity (f a b c)) r1 r2)