packages feed

large-records-0.1.0.0: src/Data/Record/Internal/Record.hs

{-# LANGUAGE DataKinds           #-}
{-# LANGUAGE DeriveFoldable      #-}
{-# LANGUAGE DeriveFunctor       #-}
{-# LANGUAGE DeriveTraversable   #-}
{-# LANGUAGE DerivingStrategies  #-}
{-# LANGUAGE RecordWildCards     #-}
{-# LANGUAGE ScopedTypeVariables #-}

module Data.Record.Internal.Record (
    -- * Record description
    Record(..)
  , Field(..)
    -- * Combinators
  , matchRecordFields
  , dropMissingRecordFields
  ) where

import Control.Monad.State
import Data.List (sortBy)
import Data.Map (Map)
import Data.Maybe (mapMaybe)
import Data.Ord (comparing)
import Language.Haskell.TH

import qualified Data.Map as Map
import qualified Data.Map.Merge.Lazy as Map

{-------------------------------------------------------------------------------
  Description
-------------------------------------------------------------------------------}

-- | Record description
data Record a = Record {
      -- | Record type name
      recordType :: String

      -- | Record constructor name
    , recordConstr :: String

      -- | Type variables in the records type
    , recordTVars :: [TyVarBndr]

      -- | Fields in the record
    , recordFields :: [Field a]
    }
  deriving stock (Show, Functor, Foldable, Traversable)

-- | Record field description
data Field a = Field {
      -- | Field name
      fieldName :: String

      -- | Type of the field
    , fieldType :: Type

      -- | Index of the field (field 0, field 1, ..)
      --
      -- This is strictly speaking redundant information, as this is already
      -- implied by the position of the field in 'recordFields'. However, since
      -- we do a lot of positional processing (every field corresponds to a
      -- vector element), it is convenient to have the index readily available.
    , fieldIndex :: Int

      -- | Value associated with this field ('Nothing' if not present)
    , fieldVal :: a
    }
  deriving stock (Show, Functor, Foldable, Traversable)

{-------------------------------------------------------------------------------
  Combinators
-------------------------------------------------------------------------------}

-- | Match field values against field definitions
--
-- We explicit mark missing fields; one use case for this is record
-- construction, where we want to issue a warning for missing fields.
matchRecordFields :: forall a b.
     [(String, b)]
  -> Record a
  -> (Record (a, Maybe b), [String])
matchRecordFields values r = (
      r { recordFields = sortBy (comparing fieldIndex) $
                               Map.elems matched
        }
    , unknown
    )
  where
    given :: Map String b
    given = Map.fromList values

    defined :: Map String (Field a)
    defined = Map.fromList $
                map (\f -> (fieldName f, f)) (recordFields r)

    matched :: Map String (Field (a, Maybe b))
    unknown :: [String]
    (matched, unknown) = flip runState [] $
        Map.mergeA
          (Map.traverseMissing      fieldMissing)
          (Map.traverseMaybeMissing fieldUnknown)
          (Map.zipWithAMatched      fieldPresent)
          defined
          given

    fieldPresent :: String -> Field a -> b -> State [String]        (Field (a, Maybe b))
    fieldMissing :: String -> Field a      -> State [String]        (Field (a, Maybe b))
    fieldUnknown :: String ->            b -> State [String] (Maybe (Field (a, Maybe b)))

    fieldPresent _nm  f  a = return $ f { fieldVal = (fieldVal f, Just a) }
    fieldMissing _nm  f    = return $ f { fieldVal = (fieldVal f, Nothing) }
    fieldUnknown  nm    _a = modify (nm:) >> return Nothing

dropMissingRecordFields :: Record (Maybe a) -> Record a
dropMissingRecordFields r =
    r { recordFields = mapMaybe sequenceA (recordFields r) }