From 6dc17473e9071ee59856ca7a7705c8a6f0a03f6f Mon Sep 17 00:00:00 2001 From: Philipp Balzarek Date: Wed, 19 May 2021 22:38:15 +0200 Subject: [PATCH 1/3] Implement improvements to ARec (#150, #153) * Move from Array to SmallArray# to avoid intermediate list during construction * Use class instead of recursive function in toARec for improved inlining --- Data/Vinyl/ARec/Internal.hs | 167 ++++++++++++++++++++----- Data/Vinyl/ARec/Internal/SmallArray.hs | 56 +++++++++ benchmarks/AccessorsBench.hs | 53 +++++--- benchmarks/Bench/ARec.hs | 39 ++++++ benchmarks/Bench/Rec.hs | 37 ++++++ benchmarks/Bench/SRec.hs | 34 +++++ tests/Aeson.hs | 2 +- tests/Spec.hs | 2 +- vinyl.cabal | 4 + 9 files changed, 346 insertions(+), 48 deletions(-) create mode 100644 Data/Vinyl/ARec/Internal/SmallArray.hs create mode 100644 benchmarks/Bench/ARec.hs create mode 100644 benchmarks/Bench/Rec.hs create mode 100644 benchmarks/Bench/SRec.hs diff --git a/Data/Vinyl/ARec/Internal.hs b/Data/Vinyl/ARec/Internal.hs index d42fda8..b30bff1 100644 --- a/Data/Vinyl/ARec/Internal.hs +++ b/Data/Vinyl/ARec/Internal.hs @@ -1,4 +1,5 @@ {-# LANGUAGE AllowAmbiguousTypes #-} +{-# LANGUAGE BangPatterns #-} {-# LANGUAGE CPP #-} {-# LANGUAGE ConstraintKinds #-} {-# LANGUAGE DataKinds #-} @@ -23,9 +24,26 @@ -- element to the head of a list, but element access is linear time; -- array access time is uniform, but extending the array is more -- slower. + +-- Tradeoffs: +-- +-- * No sharing of the spine (i.e. when you change elements in the front of the +-- record the tail can't be re-used) +-- * ARec requires (4 + n) words + size of the fields +-- * 1 for the ARec constructor +-- * 1 for the pointer to the SmallArray# +-- * The SmallArray# has 2 words as header (1 for GC, 1 for number of elements) +-- * 1 pointer per element to the actual data +-- * Rec requires (2n) words + size of Fields +-- * 1 word per (:&) constructor +-- * 1 word for the pointer to the element module Data.Vinyl.ARec.Internal ( ARec (..) , IndexableField + , arec + , ARecBuilder (..) + , arcons + , arnil , toARec , fromARec , aget @@ -39,28 +57,36 @@ module Data.Vinyl.ARec.Internal import Data.Vinyl.Core import Data.Vinyl.Lens (RecElem(..), RecSubset(..)) import Data.Vinyl.TypeLevel +import Data.Vinyl.ARec.Internal.SmallArray +import Control.Monad.ST -import qualified Data.Array as Array -import qualified Data.Array.Base as BArray -import GHC.Exts (Any) import Unsafe.Coerce #if __GLASGOW_HASKELL__ < 806 import Data.Constraint.Forall (Forall) #endif -import Data.Coerce (Coercible) -import Data.Type.Coercion (Coercion (..)) +import Data.Type.Coercion (Coercion (..)) +import GHC.Types -- | An array-backed extensible record with constant-time field -- access. -newtype ARec (f :: k -> *) (ts :: [k]) = ARec (Array.Array Int Any) +newtype ARec (f :: k -> *) (ts :: [k]) = ARec SmallArray type role ARec representational nominal +-- | Get the ith element from the ARec +unsafeIxARec + :: forall a k (f :: k -> *) (ts :: [k]). + ARec f ts + -> Int + -> a +unsafeIxARec (ARec ar) ix = indexSmallArray ar ix +{-# INLINE unsafeIxARec #-} + -- | Given that @xs@ and @ys@ have the same length, and mapping -- @f@ over @xs@ and @g@ over @ys@ produces lists whose elements -- are pairwise 'Coercible', @ARec f xs@ and @ARec g ys@ are -- 'Coercible'. arecRepsMatchCoercion :: AllRepsMatch f xs g ys => Coercion (ARec f xs) (ARec g ys) -arecRepsMatchCoercion = Coercion +arecRepsMatchCoercion = unsafeCoerce (Coercion :: Coercion () ()) -- | Given that @forall x. Coercible (f x) (g x)@, produce a coercion from -- @ARec f xs@ to @ARec g xs@. While the constraint looks a lot like @@ -69,7 +95,7 @@ arecRepsMatchCoercion = Coercion #if __GLASGOW_HASKELL__ >= 806 arecConsMatchCoercion :: (forall (x :: k). Coercible (f x) (g x)) => Coercion (ARec f xs) (ARec g xs) -arecConsMatchCoercion = Coercion +arecConsMatchCoercion = unsafeCoerce (Coercion :: Coercion () ()) #else arecConsMatchCoercion :: forall k (f :: k -> *) (g :: k -> *) (xs :: [k]). Forall (Similar f g) => Coercion (Rec f xs) (Rec g xs) @@ -78,6 +104,28 @@ arecConsMatchCoercion :: forall k (f :: k -> *) (g :: k -> *) (xs :: [k]). arecConsMatchCoercion = unsafeCoerce (Coercion :: Coercion (Rec f xs) (Rec f xs)) #endif +-- Using a class instead of a recursive function allows aRecValues to be +-- completely inlined +class ToARec (us :: [k]) where + aRecValues :: Rec f us -> ARecBuilder f us + +instance ToARec '[] where + aRecValues RNil = arnil + {-# INLINE aRecValues #-} + +instance ToARec us => ToARec (u ': us) where + aRecValues (x :& xs) = x `arcons` aRecValues xs + {-# INLINE aRecValues #-} + +-- | Convert a 'Rec' into an 'ARec' for constant-time field access. +toARec + :: forall f ts. + (NatToInt (RLength ts), ToARec ts) + => Rec f ts + -> ARec f ts +toARec rs = arec (aRecValues rs) +{-# INLINE toARec #-} + {- -- This is sensible, but the ergonomics are likely quite bad thanks to the -- interaction between Coercible resolution and resolution in the presence of @@ -89,14 +137,63 @@ arecConsMatchCoercible :: forall k f g rep (r :: TYPE rep). arecConsMatchCoercible f = f -} --- | Convert a 'Rec' into an 'ARec' for constant-time field access. -toARec :: forall f ts. (NatToInt (RLength ts)) => Rec f ts -> ARec f ts -toARec = go id - where go :: ([Any] -> [Any]) -> Rec f ts' -> ARec f ts - go acc RNil = ARec $! Array.listArray (0, n - 1) (acc []) - go acc (x :& xs) = go (acc . (unsafeCoerce x :)) xs - n = natToInt @(RLength ts) -{-# INLINE toARec #-} +-- | An efficient builder for ARec values +-- +-- Use the pseudo-constructors 'arcons' and 'arnil' to construct an +-- 'ARecBuilder' and then turn it into an 'ARec' with 'arec' +-- +-- Example: (requires -XOverloadedLabels and ) +-- +-- > user :: ARec ElField '[ "name" ::: String +-- > , "age" ::: Int +-- > , "active" ::: Bool] +-- > user = arec ( #name =: "Peter" +-- > `arcons` #age =: 4 +-- > `arcons` #active =: True +-- > `arcons` arnil +-- > ) +newtype ARecBuilder f us = + -- A function that writes values to the correct position in the underlying array + -- Takes the current index + ARecBuilder ( forall s. + Int -- Index to write to + -> SmallMutableArray s -- Arrray to write to + -> ST s () + ) + +infixr 1 `arcons` +-- | Pseudo-constructor for an ARecBuilder +-- +-- "Cons" a field to an ARec under construction +-- +-- See 'ARecBuilder' +arcons :: f u -> ARecBuilder f us -> ARecBuilder f (u ': us) +arcons !v (ARecBuilder fvs) = ARecBuilder $ \i mArr -> do + writeSmallArray mArr i v + fvs (i+1) mArr +{-# INLINE arcons #-} + +-- | Pseudo-constructor for 'ARecBuilder' +-- +-- Build an ARec without fields +-- +-- See 'ARecBuilder' +arnil :: ARecBuilder f '[] +arnil = ARecBuilder $ \_i _arr -> return () +{-# INLINE arnil #-} + +-- | Turn an ARecBuilder into an ARec +-- +-- See 'ARecBuilder' +arec + :: forall k (us :: [k] ) f + . (NatToInt (RLength us)) => + ARecBuilder f us + -> ARec f us +arec (ARecBuilder fillArray) = ARec $ + runST $ withNewSmallArray (natToInt @(RLength us)) + $ fillArray 0 +{-# INLINE arec #-} -- | Defines a constraint that lets us index into an 'ARec' in order -- to produce a 'Rec' using 'fromARec'. @@ -107,22 +204,22 @@ instance (NatToInt (RIndex t ts)) => IndexableField ts t where fromARec :: forall f ts. (RecApplicative ts, RPureConstrained (IndexableField ts) ts) => ARec f ts -> Rec f ts -fromARec (ARec arr) = rpureConstrained @(IndexableField ts) aux +fromARec ar = rpureConstrained @(IndexableField ts) aux where aux :: forall t. NatToInt (RIndex t ts) => f t - aux = unsafeCoerce (arr Array.! natToInt @(RIndex t ts)) + aux = unsafeIxARec ar (natToInt @(RIndex t ts)) {-# INLINE fromARec #-} -- | Get a field from an 'ARec'. aget :: forall t f ts. (NatToInt (RIndex t ts)) => ARec f ts -> f t -aget (ARec arr) = - unsafeCoerce (BArray.unsafeAt arr (natToInt @(RIndex t ts))) +aget ar = unsafeIxARec ar (natToInt @(RIndex t ts)) {-# INLINE aget #-} -- | Set a field in an 'ARec'. unsafeAput :: forall t t' f ts ts'. (NatToInt (RIndex t ts)) => f t' -> ARec f ts -> ARec f ts' -unsafeAput x (ARec arr) = ARec (arr Array.// [(i, unsafeCoerce x)]) - where i = natToInt @(RIndex t ts) +unsafeAput x (ARec arr) = ARec $ runST $ + withThawedSmallArray arr $ \mArr -> + writeSmallArray mArr (natToInt @(RIndex t ts)) x {-# INLINE unsafeAput #-} -- | Define a lens for a field of an 'ARec'. @@ -157,19 +254,31 @@ instance (RIndex t (s ': ts) ~ 'S i, NatToInt i, RecElem ARec t t' ts ts' i) arecGetSubset :: forall rs ss f. (IndexWitnesses (RImage rs ss), NatToInt (RLength rs)) => ARec f ss -> ARec f rs -arecGetSubset (ARec arr) = ARec (Array.listArray (0, n-1) $ - go (indexWitnesses @(RImage rs ss))) - where go :: [Int] -> [Any] - go = map (arr Array.!) - n = natToInt @(RLength rs) +arecGetSubset (ARec arr) = + ARec $ runST $ + withNewSmallArray (natToInt @(RLength rs)) $ \mArr -> + go mArr 0 (indexWitnesses @(RImage rs ss)) + where + go :: SmallMutableArray s -> Int -> [Int] -> ST s () + go _mArr _to [] = return () + go mArr to (from : froms) = do + writeSmallArray mArr to (indexSmallArray arr from :: Any) + go mArr (to + 1) froms {-# INLINE arecGetSubset #-} -- | Set a subset of a larger record's fields to all of the fields of -- a smaller record. arecSetSubset :: forall rs ss f. (IndexWitnesses (RImage rs ss)) => ARec f ss -> ARec f rs -> ARec f ss -arecSetSubset (ARec arrBig) (ARec arrSmall) = ARec (arrBig Array.// updates) - where updates = zip (indexWitnesses @(RImage rs ss)) (Array.elems arrSmall) +arecSetSubset (ARec arrBig) (ARec arrSmall) = ARec $ runST $ + withThawedSmallArray arrBig $ \mArr -> do + go mArr 0 (indexWitnesses @(RImage rs ss)) + where + go :: SmallMutableArray s -> Int -> [Int] -> ST s () + go _mArr _ [] = return () + go mArr from (to : tos) = do + writeSmallArray mArr to (indexSmallArray arrSmall from) + go mArr (from + 1) tos {-# INLINE arecSetSubset #-} instance (is ~ RImage rs ss, IndexWitnesses is, NatToInt (RLength rs)) diff --git a/Data/Vinyl/ARec/Internal/SmallArray.hs b/Data/Vinyl/ARec/Internal/SmallArray.hs new file mode 100644 index 0000000..00a81fc --- /dev/null +++ b/Data/Vinyl/ARec/Internal/SmallArray.hs @@ -0,0 +1,56 @@ +{-# LANGUAGE MagicHash #-} +{-# LANGUAGE UnboxedTuples #-} +{-# LANGUAGE RankNTypes #-} +{-# LANGUAGE BangPatterns #-} + +-- | Helper functions for SmallArray# +-- +-- This module exposes _unsafe_ functions to work with SmallArrays. That means +-- that specifically neither index bounds nor element types are checked So this +-- functionality should only be used in a context that enforces them by some +-- other means, e.g. ARec's type index + +module Data.Vinyl.ARec.Internal.SmallArray where + +import GHC.Prim +import GHC.Types +import Unsafe.Coerce +import GHC.ST + +data SmallArray = SmallArray !(SmallArray# Any) +data SmallMutableArray s = SmallMutableArray !(SmallMutableArray# s Any) + +indexSmallArray :: SmallArray -> Int -> a +indexSmallArray (SmallArray arr) (I# ix) = + case indexSmallArray# arr ix of + (# v #) -> unsafeCoerce v +{-# INLINE indexSmallArray #-} + +withNewSmallArray :: Int -> (SmallMutableArray s -> ST s ()) -> ST s SmallArray +withNewSmallArray (I# len#) f = + ST $ \s0 -> case newSmallArray# len# (error "withNewSmallArray exploded") s0 of + (# s1, mArr #) -> + case f (SmallMutableArray mArr) of + ST st -> case st s1 of + (# s2, () #) -> case unsafeFreezeSmallArray# mArr s2 of + (# s3, ar #) -> (# s3, SmallArray ar #) +{-# INLINE withNewSmallArray #-} + +writeSmallArray :: SmallMutableArray s -> Int -> a -> ST s () +writeSmallArray (SmallMutableArray mArr) (I# n#) x = ST $ \s -> + case writeSmallArray# mArr n# (unsafeCoerce x) s of + s' -> (# s', () #) +{-# INLINE writeSmallArray #-} + +withThawedSmallArray :: SmallArray + -> (SmallMutableArray s -> ST s ()) + -> ST s SmallArray +withThawedSmallArray (SmallArray arr) f = ST $ \s0 -> + let !(I# z#) = 0 + in case thawSmallArray# arr z# (sizeofSmallArray# arr) s0 of + (# s1, mArr #) -> + case f (SmallMutableArray mArr) of + ST st -> case st s1 of + (# s2, () #) -> case unsafeFreezeSmallArray# mArr s2 of + (# s3, ar #) -> (# s3, SmallArray ar #) +{-# INLINE withThawedSmallArray #-} diff --git a/benchmarks/AccessorsBench.hs b/benchmarks/AccessorsBench.hs index bde2e64..71b625b 100644 --- a/benchmarks/AccessorsBench.hs +++ b/benchmarks/AccessorsBench.hs @@ -8,21 +8,12 @@ import Criterion.Main import Data.Monoid (Endo(..)) import Data.Vinyl import Data.Vinyl.Syntax () -import Lens.Micro ((%~), (&)) -import System.Exit (exitFailure) - -type Fields = '[ '( "a0", Int ), '( "a1", Int ), '( "a2", Int ), '( "a3", Int ) - , '( "a4", Int ), '( "a5", Int ), '( "a6", Int ), '( "a7", Int ) - , '( "a8", Int ), '( "a9", Int ), '( "a10", Int ), '( "a11", Int ) - , '( "a12", Int ), '( "a13", Int ), '( "a14", Int ), '( "a15", Int ) - ] - -newF :: FieldRec Fields -newF = Field 0 :& Field 0 :& Field 0 :& Field 0 :& - Field 0 :& Field 0 :& Field 0 :& Field 0 :& - Field 0 :& Field 0 :& Field 0 :& Field 0 :& - Field 0 :& Field 0 :& Field 0 :& Field 99 :& - RNil +import Lens.Micro ((%~), (&)) +import System.Exit (exitFailure) + +import Bench.ARec +import Bench.SRec +import Bench.Rec data HaskRec = HaskRec { a0 :: Int, @@ -45,6 +36,10 @@ data HaskRec = HaskRec { haskRec :: HaskRec haskRec = HaskRec 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 99 +sumHaskRec r = + a0 r + a1 r + a2 r + a3 r + a4 r + a5 r + a6 r + a7 r + a8 r + a9 r + + a10 r + a11 r + a12 r + a13 r + a14 r + a15 r + data StrictHaskRec = StrictHaskRec { sa0 :: !Int, sa1 :: !Int, @@ -66,6 +61,10 @@ data StrictHaskRec = StrictHaskRec { shaskRec :: StrictHaskRec shaskRec = StrictHaskRec 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 99 +sumSHaskRec r = + sa0 r + sa1 r + sa2 r + sa3 r + sa4 r + sa5 r + sa6 r + sa7 r + sa8 r + sa9 r + + sa10 r + sa11 r + sa12 r + sa13 r + sa14 r + sa15 r + data UStrictHaskRec = UStrictHaskRec { usa0 :: {-# UNPACK #-} !Int, usa1 :: {-# UNPACK #-} !Int, @@ -87,6 +86,10 @@ data UStrictHaskRec = UStrictHaskRec { ushaskRec :: UStrictHaskRec ushaskRec = UStrictHaskRec 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 99 +sumUSHaskRec r = + usa0 r + usa1 r + usa2 r + usa3 r + usa4 r + usa5 r + usa6 r + usa7 r + usa8 r + + usa9 r + usa10 r + usa11 r + usa12 r + usa13 r + usa14 r + usa15 r + type SubFields = '[ '("a0", Int), '("a8", Int), '("a15", Int)] -- updateSRec :: forall record. RecordSubset record ElField SubFields Fields @@ -115,7 +118,8 @@ updateHaskRec r = r { a0 = suba0 s, a8 = suba8 s, a15 = suba15 s } main :: IO () main = - do let arec = toARec newF + do let newF = mkRec 0 + arec = toARec newF srec = toSRec newF unless (rvalf #a15 arec == rvalf #a15 newF) (do putStrLn "AFieldRec accessor disagrees with rvalf" @@ -142,7 +146,22 @@ main = , bench "ARec" $ nf (rvalf #a15 . updateARec) arec , bench "SRec" $ nf (rvalf #a15 . updateSRec) srec ] - , bgroup "FieldRec" + , + bgroup "creating" + [ bench "vinyl record" $ whnf mkRec 0 + , bench "toSRec" $ whnf mkToSRec 0 + , bench "New style ARec with toARec " $ whnf mkToARec 0 + , bench "New style ARec with arec " $ whnf mkARec 0 + ] + ,bgroup "sums" + [ bench "haskell record" $ nf sumHaskRec haskRec + , bench "strict haskell record" $ whnf sumSHaskRec shaskRec + , bench "unboxed strict haskell record" $ whnf sumUSHaskRec ushaskRec + , bench "vinyl SRec" $ nf sumSRec srec + , bench "vinyl Rec" $ nf sumRec newF + , bench "vinyl ARec" $ nf sumARec arec + ] + , bgroup "FieldRec" [ bench "a0" $ nf (rvalf #a0) newF , bench "a4" $ nf (rvalf #a4) newF , bench "a8" $ nf (rvalf #a8) newF diff --git a/benchmarks/Bench/ARec.hs b/benchmarks/Bench/ARec.hs new file mode 100644 index 0000000..bf493f9 --- /dev/null +++ b/benchmarks/Bench/ARec.hs @@ -0,0 +1,39 @@ +{-# LANGUAGE DataKinds #-} +{-# LANGUAGE FlexibleContexts #-} +{-# LANGUAGE OverloadedLabels #-} +{-# LANGUAGE ScopedTypeVariables #-} +{-# LANGUAGE TypeApplications #-} +{-# LANGUAGE GADTs #-} + +module Bench.ARec where + +import Data.Vinyl +import Data.Vinyl.ARec.Internal +import Data.Vinyl.Syntax () + +import Bench.Rec + +mkARec :: Int -> ARec ElField Fields +mkARec i= arec (Field i `arcons` Field i `arcons` Field i `arcons` Field i `arcons` + Field i `arcons` Field i `arcons` Field i `arcons` Field i `arcons` + Field i `arcons` Field i `arcons` Field i `arcons` Field i `arcons` + Field i `arcons` Field i `arcons` Field i `arcons` Field 99 `arcons` + arnil) + + +mkToARec :: Int -> ARec ElField Fields +mkToARec i= toARec (Field i :& Field i :& Field i :& Field i :& + Field i :& Field i :& Field i :& Field i :& + Field i :& Field i :& Field i :& Field i :& + Field i :& Field i :& Field i :& Field 99 :& + RNil) + +sumARec :: ARec ElField Fields -> Int +sumARec str = + get #a0 str + get #a1 str + get #a2 str + get #a3 str + get #a4 str + + get #a5 str + get #a6 str + get #a7 str + get #a8 str + + get #a9 str + get #a10 str + get #a11 str + get #a12 str + + get #a13 str + get #a14 str + get #a15 str + where + get label r = rvalf label r + {-# INLINE get #-} diff --git a/benchmarks/Bench/Rec.hs b/benchmarks/Bench/Rec.hs new file mode 100644 index 0000000..3856b5a --- /dev/null +++ b/benchmarks/Bench/Rec.hs @@ -0,0 +1,37 @@ +{-# LANGUAGE DataKinds #-} +{-# LANGUAGE FlexibleContexts #-} +{-# LANGUAGE OverloadedLabels #-} +{-# LANGUAGE ScopedTypeVariables #-} +{-# LANGUAGE TypeApplications #-} +{-# LANGUAGE GADTs #-} + +module Bench.Rec where + +import Data.Vinyl +import Data.Vinyl.Syntax () + + +type Fields = '[ '( "a0", Int ), '( "a1", Int ), '( "a2", Int ), '( "a3", Int ) + , '( "a4", Int ), '( "a5", Int ), '( "a6", Int ), '( "a7", Int ) + , '( "a8", Int ), '( "a9", Int ), '( "a10", Int ), '( "a11", Int ) + , '( "a12", Int ), '( "a13", Int ), '( "a14", Int ), '( "a15", Int ) + ] + +mkRec :: Int -> Rec ElField Fields +mkRec i= Field i :& Field i :& Field i :& Field i :& + Field i :& Field i :& Field i :& Field i :& + Field i :& Field i :& Field i :& Field i :& + Field i :& Field i :& Field i :& Field 99 :& + RNil + +sumRec :: Rec ElField Fields -> Int +sumRec str = + get #a0 str + get #a1 str + get #a2 str + get #a3 str + get #a4 str + + get #a5 str + get #a6 str + get #a7 str + get #a8 str + + get #a9 str + get #a10 str + get #a11 str + get #a12 str + + get #a13 str + get #a14 str + get #a15 str + where + get (_label :: Label s) r = + let (Field v) = rget @'(s, _) r + in v + {-# INLINE get #-} diff --git a/benchmarks/Bench/SRec.hs b/benchmarks/Bench/SRec.hs new file mode 100644 index 0000000..7cd8c57 --- /dev/null +++ b/benchmarks/Bench/SRec.hs @@ -0,0 +1,34 @@ +{-# LANGUAGE DataKinds #-} +{-# LANGUAGE FlexibleContexts #-} +{-# LANGUAGE OverloadedLabels #-} +{-# LANGUAGE ScopedTypeVariables #-} +{-# LANGUAGE TypeApplications #-} +{-# LANGUAGE GADTs #-} + +module Bench.SRec where + +import Data.Vinyl.SRec +import Data.Vinyl + +import Bench.Rec (Fields) + + +mkToSRec :: Int -> SRec ElField Fields +mkToSRec i= toSRec (Field i :& Field i :& Field i :& Field i :& + Field i :& Field i :& Field i :& Field i :& + Field i :& Field i :& Field i :& Field i :& + Field i :& Field i :& Field i :& Field 99 :& + RNil) + + +sumSRec :: SRec ElField Fields -> Int +sumSRec str = + get #a0 str + get #a1 str + get #a2 str + get #a3 str + get #a4 str + + get #a5 str + get #a6 str + get #a7 str + get #a8 str + + get #a9 str + get #a10 str + get #a11 str + get #a12 str + + get #a13 str + get #a14 str + get #a15 str + where + get (label :: Label s) r = + case rget @'(s, Int) r of + Field v -> v + {-# INLINE get #-} diff --git a/tests/Aeson.hs b/tests/Aeson.hs index aae8fbf..b3a946f 100644 --- a/tests/Aeson.hs +++ b/tests/Aeson.hs @@ -62,7 +62,7 @@ instance ToJSON a => ToJSON (Identity a) where -- | A named field serializes to a JSON object with a single named -- field. -instance ToJSON a => ToJSON (ElField '(s,a)) where +instance (KnownSymbol s, ToJSON a) => ToJSON (ElField '(s,a)) where toJSON x = object [(T.pack (getLabel x), toJSON (getField x))] -- | A @((Text,) :. f) a@ value maps to a JSON field whose name is the diff --git a/tests/Spec.hs b/tests/Spec.hs index 0d3c61a..aa9666e 100644 --- a/tests/Spec.hs +++ b/tests/Spec.hs @@ -37,7 +37,7 @@ main = hspec $ do describe "Fields may be accessed by overloaded labels" $ do it "Can get field X" $ rvalf #x d3 `shouldBe` 5 it "Can get field Y" $ rvalf #y d3 `shouldBe` "Hi" - describe "ARec provides field accessors" $ do + describe "ARec" $ do it "Can get field Y" $ rvalf #y (toARec d3) `shouldBe` "Hi" it "Can set field X" $ rvalf #x (rputf #x 7 (toARec d3)) `shouldBe` 7 describe "Converting between Rec and ARec" $ do diff --git a/vinyl.cabal b/vinyl.cabal index 0abe9a7..cdd2f3c 100644 --- a/vinyl.cabal +++ b/vinyl.cabal @@ -24,6 +24,7 @@ library exposed-modules: Data.Vinyl , Data.Vinyl.ARec , Data.Vinyl.ARec.Internal + , Data.Vinyl.ARec.Internal.SmallArray , Data.Vinyl.Class.Method , Data.Vinyl.Core , Data.Vinyl.CoRec @@ -78,6 +79,9 @@ benchmark accessors hs-source-dirs: benchmarks main-is: AccessorsBench.hs build-depends: base, criterion, tagged, vinyl, microlens + other-modules: Bench.ARec + Bench.SRec + Bench.Rec ghc-options: -O2 default-language: Haskell2010 From 5e380747f5922160b486fef58ba1cc6993a3273c Mon Sep 17 00:00:00 2001 From: Philipp Balzarek Date: Wed, 19 May 2021 22:55:33 +0200 Subject: [PATCH 2/3] Turn ElField into a newtype (#150) * Saves a 1 word per field * Improves accessor performance significantly both for Rec as well as ARec --- Data/Vinyl/Class/Method.hs | 2 +- Data/Vinyl/Derived.hs | 2 +- Data/Vinyl/Functor.hs | 7 +++++-- 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/Data/Vinyl/Class/Method.hs b/Data/Vinyl/Class/Method.hs index d10774a..212c72d 100644 --- a/Data/Vinyl/Class/Method.hs +++ b/Data/Vinyl/Class/Method.hs @@ -142,7 +142,7 @@ data FieldTyper = FieldId | FieldSnd -- | The interpretation function of the 'FieldTyper' symbols. type family ApplyFieldTyper (f :: FieldTyper) (a :: k) :: * where ApplyFieldTyper 'FieldId a = a - ApplyFieldTyper 'FieldSnd '(s, b) = b + ApplyFieldTyper 'FieldSnd a = Snd a -- | A mapping of record contexts into the 'FieldTyper' function -- space. We explicitly match on 'ElField' to pick out the payload diff --git a/Data/Vinyl/Derived.hs b/Data/Vinyl/Derived.hs index a1bd071..6ed7582 100644 --- a/Data/Vinyl/Derived.hs +++ b/Data/Vinyl/Derived.hs @@ -46,7 +46,7 @@ getField :: ElField '(s,t) -> t getField (Field x) = x -- | Get the label name of an 'ElField'. -getLabel :: forall s t. ElField '(s,t) -> String +getLabel :: forall s t. KnownSymbol s => ElField '(s,t) -> String getLabel (Field _) = symbolVal (Proxy::Proxy s) -- | 'ElField' is isomorphic to a functor something like @Compose diff --git a/Data/Vinyl/Functor.hs b/Data/Vinyl/Functor.hs index 8d66021..179fd8a 100644 --- a/Data/Vinyl/Functor.hs +++ b/Data/Vinyl/Functor.hs @@ -42,6 +42,7 @@ import Foreign.Storable import GHC.Generics import GHC.TypeLits import GHC.Types (Type) +import Data.Vinyl.TypeLevel (Snd) {- $introduction This module provides functors and functor compositions @@ -107,8 +108,10 @@ newtype Const (a :: *) (b :: k) -- | A value with a phantom 'Symbol' label. It is not a -- Haskell 'Functor', but it is used in many of the same places a -- 'Functor' is used in vinyl. -data ElField (field :: (Symbol, Type)) where - Field :: KnownSymbol s => !t -> ElField '(s,t) +-- +-- Morally: newtype ElField (s, t) = Field t +-- But GHC doesn't allow that +newtype ElField (t :: (Symbol, Type)) = Field (Snd t) deriving instance Eq t => Eq (ElField '(s,t)) deriving instance Ord t => Ord (ElField '(s,t)) From 5aeff457aa9407874961a4a8c3b0ac940974b5de Mon Sep 17 00:00:00 2001 From: Philipp Balzarek Date: Wed, 26 May 2021 22:47:30 +0200 Subject: [PATCH 3/3] Add test cases --- tests/Spec.hs | 6 ++- tests/Test/ARec.hs | 101 +++++++++++++++++++++++++++++++++++++++++++++ vinyl.cabal | 4 +- 3 files changed, 109 insertions(+), 2 deletions(-) create mode 100644 tests/Test/ARec.hs diff --git a/tests/Spec.hs b/tests/Spec.hs index aa9666e..766d732 100644 --- a/tests/Spec.hs +++ b/tests/Spec.hs @@ -11,6 +11,8 @@ import Data.Vinyl.Syntax () import qualified CoRecSpec as C import qualified XRecSpec as X +import qualified Test.ARec as ARec + -- d1 :: FieldRec '[ '("X",String), '("Y", String) ] -- d1 = Field @"X" "5" :& Field @"Y" "Hi" :& RNil @@ -37,7 +39,7 @@ main = hspec $ do describe "Fields may be accessed by overloaded labels" $ do it "Can get field X" $ rvalf #x d3 `shouldBe` 5 it "Can get field Y" $ rvalf #y d3 `shouldBe` "Hi" - describe "ARec" $ do + describe "ARec provides field accessors" $ do it "Can get field Y" $ rvalf #y (toARec d3) `shouldBe` "Hi" it "Can set field X" $ rvalf #x (rputf #x 7 (toARec d3)) `shouldBe` 7 describe "Converting between Rec and ARec" $ do @@ -71,3 +73,5 @@ main = hspec $ do (#x .~ 2.1) d3 `shouldBe` fieldRec (#x =: 2.1, #y =: "Hi") it "Can change a field's type" $ (d3 & #y %~ length) `shouldBe` fieldRec (#x =: 5, #y =: 2) + + ARec.spec diff --git a/tests/Test/ARec.hs b/tests/Test/ARec.hs new file mode 100644 index 0000000..8fa5526 --- /dev/null +++ b/tests/Test/ARec.hs @@ -0,0 +1,101 @@ +{-# LANGUAGE DataKinds, FlexibleContexts, GADTs, + NoMonomorphismRestriction, OverloadedLabels, + ScopedTypeVariables, TypeApplications, TypeOperators #-} +{-# OPTIONS_GHC -Wall -Wno-type-defaults #-} + +module Test.ARec where + +import Data.Vinyl.ARec +import Data.Vinyl +import Test.Hspec + +import Data.Vinyl.Syntax () + +type FullARec = ARec ElField '[ "f0" ::: Int , "f1" ::: Bool , "f2" ::: String + , "f3" ::: Double, "f4" ::: Integer + , "f2" ::: Int -- intentionally duplicate field name + ] + +type SubARecPre = ARec ElField '[ "f0" ::: Int , "f1" ::: Bool , "f2" ::: String ] + +type SubARecDupes = ARec ElField '[ "f2" ::: String, "f2" ::: String + , "f2" ::: Int, "f2" ::: String + ] + + +fullARec :: FullARec +fullARec = toARec ( #f0 =: 1 :& #f1 =: False :& #f2 =: "field2" + :& #f3 =: 3.1415 :& #f4 =: 4444 + :& #f2 =: 666 + :& RNil + ) + +-- For arecGetSubset ----------------------------------------------------------- + +subARecPre :: SubARecPre +subARecPre = toARec ( #f0 =: 1 :& #f1 =: False :& #f2 =: "field2" :& RNil) + +subARecDupes :: SubARecDupes +subARecDupes = toARec ( #f2 =: "field2" :& #f2 =: "field2" + :& #f2 =: 666 :& #f2 =: "field2" + :& RNil + ) + +arecWithDupes :: ARec ElField '[ "f" ::: Int, "f" ::: Int] +arecWithDupes = toARec (#f =: 1 :& #f =: 2 :& RNil) + +-- For arecSetSubset ----------------------------------------------------------- + +subARecPreSet :: SubARecPre +subARecPreSet = toARec ( #f0 =: 11 :& #f1 =: True :& #f2 =: "field2-updated" :& RNil) + +fullARecUpdated :: FullARec +fullARecUpdated = toARec ( #f0 =: 11 :& #f1 =: True :& #f2 =: "field2-updated" + :& #f3 =: 3.1415 :& #f4 =: 4444 + :& #f2 =: 666 + :& RNil + ) + +updateARecWithDupes :: ARec ElField '[ '("f0", Int), '("f0", Int), '("f0", Int)] +updateARecWithDupes = toARec (#f0 =: 3 :& #f0 =: 66 :& #f0 =: 1 :&RNil) + +subARecDupesUpdated :: SubARecDupes +subARecDupesUpdated = toARec ( #f2 =: "updated" :& #f2 =: "field2" + :& #f2 =: 666 :& #f2 =: "field2" + :& RNil + ) + + + +spec :: SpecWith () +spec = describe "ARec" $ do + describe "arecGetSubset" $ do + it "retrieves a prefix ARec" $ + -- The part to be retrieved is type-directed + arecGetSubset fullARec `shouldBe` subARecPre + it "retrieves the full ARec" $ do + -- Should catch off-by-one errors that lead to overflow + arecGetSubset fullARec `shouldBe` fullARec + it "handles an empty subARec correctly" $ + arecGetSubset fullARec `shouldBe` toARec RNil + it "handles duplicate field names correctly in the sub arec" $ + arecGetSubset fullARec `shouldBe` subARecDupes + it "handles duplicate field names correctly in the source arec" $ + -- When both the name and the type of the field match we retrieve from the + -- first field + arecGetSubset arecWithDupes `shouldBe` toARec (#f =: (1 :: Int) :& RNil) + describe "arecSetSubset" $ do + it "sets a subset of fields" $ do + arecSetSubset fullARec subARecPreSet `shouldBe` fullARecUpdated + it "handles updates to every field" $ do + -- Should catch off-by-one errors that lead to overflow + arecSetSubset fullARec fullARec `shouldBe` fullARec + it "handles an empty subset" $ do + arecSetSubset fullARec (toARec RNil) `shouldBe` fullARec + it "handles duplicates in the updating ARec" $ do + -- The behaviour here should be that the _last_ updating field prevails + arecSetSubset fullARec updateARecWithDupes `shouldBe` fullARec + it "handles updatees with duplicate fields" $ do + -- Here, only the _first_ field should be updated + arecSetSubset subARecDupes (toARec (#f2 =: "updated" :& RNil)) + `shouldBe` subARecDupesUpdated diff --git a/vinyl.cabal b/vinyl.cabal index cdd2f3c..ce9f418 100644 --- a/vinyl.cabal +++ b/vinyl.cabal @@ -117,7 +117,9 @@ test-suite spec type: exitcode-stdio-1.0 hs-source-dirs: tests main-is: Spec.hs - other-modules: CoRecSpec XRecSpec + other-modules: CoRecSpec + XRecSpec + Test.ARec build-depends: base , vinyl , microlens