-- |Various kinds of arrays (lists, vectors, bytestrings) with statically aserted length constraints encoded in their type. {-# LANGUAGE DataKinds #-} {-# LANGUAGE ExplicitNamespaces #-} {-# LANGUAGE GeneralizedNewtypeDeriving #-} {-# LANGUAGE KindSignatures #-} {-# LANGUAGE ScopedTypeVariables #-} {-# LANGUAGE TypeFamilies #-} {-# LANGUAGE TypeOperators #-} -- {-# OPTIONS_GHC -Wno-redundant-constraints #-} module Network.ONCRPC.XDR.Array ( KnownNat , KnownOrdering , OpaqueString(..) , opaqueLengthArray , unOpaqueLengthArray , LengthArray , FixedLengthArray , fixedLengthArrayLength , BoundedLengthArray , boundedLengthArrayBound , unLengthArray , unsafeLengthArray , lengthArray , lengthArray' , boundLengthArray , boundLengthArrayFromList , padLengthArray , constLengthArray , emptyFixedLengthArray , emptyBoundedLengthArray , expandBoundedLengthArray , boundFixedLengthArray , appendLengthArray , fromLengthList ) where import Prelude hiding (length, take, drop, replicate) import Control.Monad (guard) import qualified Data.ByteString as BS import qualified Data.ByteString.Lazy as BSL import qualified Data.ByteString.Base16 as Hex import qualified Data.List as List import Data.Maybe (fromMaybe, fromJust) import Data.Monoid (Monoid, (<>)) import Data.Proxy (Proxy(..)) import Data.Semigroup (Semigroup) import Data.String (IsString(..)) import qualified Data.Vector as V import Data.Word (Word8) import GHC.TypeLits (Nat, KnownNat, natVal, type (+), type CmpNat) import Text.Read (readPrec) -- |A 'ByteString' that uses hex (base16) for 'Read'/'Show'. newtype OpaqueString = OpaqueString{ unOpaqueString :: BS.ByteString } deriving (Eq, Ord, Semigroup, Monoid, HasLength) instance Show OpaqueString where show = show . Hex.encode . unOpaqueString showsPrec p = showsPrec p . Hex.encode . unOpaqueString instance Read OpaqueString where readPrec = do b <- readPrec either fail (return . OpaqueString) $ Hex.decode b -- |Allows either hex or character input, dynamically. instance IsString OpaqueString where fromString s = OpaqueString $ either (\_ -> b) id $ Hex.decode b -- | all isHexDigit s = OpaqueString $ fst $ Hex.decode $ fromString s where b = fromString s -- See also MonoFoldable class HasLength a where length :: a -> Int -- |Equivalent to @'compare' . 'length'@ but allows more efficient implementations compareLength :: a -> Int -> Ordering compareLength = compare . length -- See also IsSquence class (Monoid a, HasLength a) => Array a where type Elem a :: * take :: Int -> a -> a replicate :: Int -> Elem a -> a fromList :: [Elem a] -> a instance HasLength [a] where length = List.length compareLength [] n = compare 0 n compareLength (_:l) n = compareLength l (n - 1) instance Array [a] where type Elem [a] = a take = List.take replicate = List.replicate fromList = id instance HasLength (V.Vector a) where length = V.length instance Array (V.Vector a) where type Elem (V.Vector a) = a take = V.take replicate = V.replicate fromList = V.fromList instance HasLength BS.ByteString where length = BS.length instance Array BS.ByteString where type Elem BS.ByteString = Word8 take = BS.take replicate = BS.replicate fromList = BS.pack instance Array OpaqueString where type Elem OpaqueString = Word8 take n = OpaqueString . BS.take n . unOpaqueString replicate n = OpaqueString . BS.replicate n fromList = OpaqueString . BS.pack instance HasLength BSL.ByteString where length = fromIntegral . BSL.length compareLength b n | BSL.null b' = LT | BSL.null (BSL.tail b') = EQ | otherwise = GT where b' = BSL.drop (fromIntegral n - 1) b instance Array BSL.ByteString where type Elem BSL.ByteString = Word8 take = BSL.take . fromIntegral replicate = BSL.replicate . fromIntegral fromList = BSL.pack class KnownOrdering (o :: Ordering) where orderingVal :: proxy o -> Ordering instance KnownOrdering 'LT where orderingVal _ = LT instance KnownOrdering 'EQ where orderingVal _ = EQ instance KnownOrdering 'GT where orderingVal _ = GT -- |Assertion that the contained array satisfies @'compareLength' a n = o@ newtype LengthArray (o :: Ordering) (n :: Nat) a = LengthArray{ unLengthArray :: a } deriving (Eq, Ord, Show) instance HasLength a => HasLength (LengthArray o n a) where length = length . unLengthArray compareLength = compareLength . unLengthArray -- |Assertion that the contained array is exactly a static length type FixedLengthArray n a = LengthArray 'EQ n a -- |Assertion that the contained array is at most a static length (inclusive) type BoundedLengthArray n a = LengthArray 'LT (n + 1) a lengthArrayOrdering :: forall o n a . KnownOrdering o => LengthArray o n a -> Ordering lengthArrayOrdering _ = orderingVal (Proxy :: Proxy o) lengthArrayBound :: forall o n a . KnownNat n => LengthArray o n a -> Int lengthArrayBound _ = fromInteger $ natVal (Proxy :: Proxy n) orderingOp :: Ordering -> Char orderingOp LT = '<' orderingOp EQ = '=' orderingOp GT = '>' describeLengthArray :: (KnownOrdering o, KnownNat n) => LengthArray o n a -> String describeLengthArray a = orderingOp (lengthArrayOrdering a) : show (lengthArrayBound a) -- |Static length of a 'FixedLengthArray' fixedLengthArrayLength :: KnownNat n => LengthArray 'EQ n a -> Int fixedLengthArrayLength = lengthArrayBound -- |Static upper-bound (inclusive) of a 'BoundedLengthArray' boundedLengthArrayBound :: KnownNat n => LengthArray 'LT n a -> Int boundedLengthArrayBound = subtract 1 . lengthArrayBound -- |Unsafely create a 'LengthArray' without checking the length bound assertion. -- May cause unpredictable behavior if the bound does not hold. unsafeLengthArray :: a -> LengthArray o n a unsafeLengthArray = LengthArray checkLengthArray :: (KnownOrdering o, KnownNat n, HasLength a) => LengthArray o n a -> Bool checkLengthArray l@(LengthArray a) = compareLength a (lengthArrayBound l) == lengthArrayOrdering l -- |Safely create a 'LengthArray' out of an array if it conforms to the static length assertion. lengthArray :: forall o n a . (KnownOrdering o, KnownNat n, HasLength a) => a -> Maybe (LengthArray o n a) lengthArray a | checkLengthArray l = Just l | otherwise = Nothing where l = LengthArray a :: LengthArray o n a -- |Create a 'LengthArray' or runtime error if the assertion fails: @fromMaybe undefined . 'lengthArray'@ lengthArray' :: forall o n a . (KnownOrdering o, KnownNat n, HasLength a) => a -> LengthArray o n a lengthArray' a = fromMaybe (error $ "lengthArray': fails check " ++ describeLengthArray (fromJust la)) la where la = lengthArray a -- |Create a 'BoundedLengthArray' by trimming the given array if necessary. boundLengthArray :: (KnownNat n, Array a) => a -> LengthArray 'LT n a boundLengthArray a = l where l = LengthArray $ take (boundedLengthArrayBound l) a -- |Create a 'BoundedLengthArray' by trimming the given array if necessary. boundLengthArrayFromList :: (KnownNat n, Array a) => [Elem a] -> LengthArray 'LT n a boundLengthArrayFromList a = l where l = LengthArray $ fromList $ take (boundedLengthArrayBound l) a -- |Create a 'FixedLengthArray' by trimming or padding (on the right) as necessary. padLengthArray :: (KnownNat n, Array a) => a -> Elem a -> LengthArray 'EQ n a padLengthArray a p = l where a' = case compareLength a n of LT -> a <> replicate (n - length a) p EQ -> a GT -> take n a n = fixedLengthArrayLength l l = LengthArray a' -- |Create a 'FixedLengthArray' filled with the same value. constLengthArray :: (KnownNat n, Array a) => Elem a -> LengthArray 'EQ n a constLengthArray p = l where l = LengthArray $ replicate (fixedLengthArrayLength l) p instance (KnownOrdering o, KnownNat n, IsString a, HasLength a) => IsString (LengthArray o n a) where fromString s = fromMaybe (error $ "String " ++ show s ++ " fails LengthArray check " ++ describeLengthArray (fromJust ls)) ls where ls = lengthArray $ fromString s -- |An empty 'FixedLengthArray'. emptyFixedLengthArray :: Array a => LengthArray 'EQ 0 a emptyFixedLengthArray = LengthArray mempty -- |An empty 'BoundedLengthArray'. emptyBoundedLengthArray :: (CmpNat 0 n ~ 'LT, Array a) => LengthArray 'LT n a emptyBoundedLengthArray = LengthArray mempty -- |Grow the bound of a 'BoundedLengthArray'. expandBoundedLengthArray :: (CmpNat n m ~ 'LT) => LengthArray 'LT n a -> LengthArray 'LT m a expandBoundedLengthArray = LengthArray . unLengthArray -- |Convert a 'FixedLengthArray' to a 'BoundedLengthArray'. boundFixedLengthArray :: (CmpNat n m ~ 'LT) => LengthArray 'EQ n a -> LengthArray 'LT m a boundFixedLengthArray = LengthArray . unLengthArray -- |Append to two 'LengthArray's. appendLengthArray :: Monoid a => LengthArray o n a -> LengthArray o m a -> LengthArray o (n + m) a appendLengthArray (LengthArray a) (LengthArray b) = LengthArray $ mappend a b fromLengthList :: Array a => LengthArray o n [Elem a] -> LengthArray o n a fromLengthList = LengthArray . fromList . unLengthArray opaqueLengthArray :: LengthArray o n BS.ByteString -> LengthArray o n OpaqueString opaqueLengthArray = LengthArray . OpaqueString . unLengthArray unOpaqueLengthArray :: LengthArray o n OpaqueString -> LengthArray o n BS.ByteString unOpaqueLengthArray = LengthArray . unOpaqueString . unLengthArray