on main help sign in
9 files changed+386−89
since splitsnapshots
unifiedsplit
.github/workflows/ci.yml+3−4 CHANGELOG.md+1−1 README.md+1−1 src/Data/TMap.hs+47−1 src/Data/TypeRepMap.hs+6−1 src/Data/TypeRepMap/Internal.hs+196−49 test/Test/TypeRep/TypeRepMap.hs+62−4 test/Test/TypeRep/TypeRepMapProperty.hs+69−26 typerep-map.cabal+1−2
.github/workflows/ci.yml+3−4
@@ -1,11 +1,10 @@
11name: CI
22
3+# Trigger the workflow on push or pull request, but only for the main branch
34on:
4− # Trigger the workflow on push or pull request,
5− # but only for the master branch
65 push:
76 branches:
8− - master
7+ - main
98 pull_request:
109
1110jobs:
@@ -24,7 +23,7 @@ jobs:
2423
2524 steps:
2625 - uses: actions/checkout@v2
27− if: github.event.action == 'opened' || github.event.action == 'synchronize' || github.event.ref == 'refs/heads/master'
26+ if: github.event.action == 'opened' || github.event.action == 'synchronize' || github.event.ref == 'refs/heads/main'
2827
2928 - uses: actions/setup-haskell@v1
3029 name: Setup Haskell
CHANGELOG.md+1−1
@@ -53,4 +53,4 @@ The changelog is available [on GitHub][2].
5353* Initially created.
5454
5555[1]: https://pvp.haskell.org
56−[2]: https://github.com/kowainik/typerep-map/blob/master/CHANGELOG.md
56+[2]: https://github.com/kowainik/typerep-map/releases
README.md+1−1
@@ -4,7 +4,7 @@
44
55[![GitHub CI](https://github.com/kowainik/typerep-map/workflows/CI/badge.svg)](https://github.com/kowainik/typerep-map/actions)
66[![Build status](https://img.shields.io/travis/kowainik/typerep-map.svg?logo=travis)](https://travis-ci.org/kowainik/typerep-map)
7−[![Windows build status](https://ci.appveyor.com/api/projects/status/github/kowainik/typerep-map?branch=master&svg=true)](https://ci.appveyor.com/project/kowainik/typerep-map)
7+[![Windows build status](https://ci.appveyor.com/api/projects/status/github/kowainik/typerep-map?branch=main&svg=true)](https://ci.appveyor.com/project/kowainik/typerep-map)
88[![Hackage](https://img.shields.io/hackage/v/typerep-map.svg?logo=haskell)](https://hackage.haskell.org/package/typerep-map)
99[![Stackage LTS](http://stackage.org/package/typerep-map/badge/lts)](http://stackage.org/lts/package/typerep-map)
1010[![Stackage Nightly](http://stackage.org/package/typerep-map/badge/nightly)](http://stackage.org/nightly/package/typerep-map)
src/Data/TMap.hs+47−1
@@ -37,14 +37,19 @@ module Data.TMap
3737 , delete
3838 , unionWith
3939 , union
40+ , intersectionWith
41+ , intersection
4042 , map
4143 , adjust
44+ , alter
4245
4346 -- * Query
4447 , lookup
4548 , member
4649 , size
4750 , keys
51+ , keysWith
52+ , toListWith
4853 ) where
4954
5055import Prelude hiding (lookup, map)
@@ -52,7 +57,7 @@ import Prelude hiding (lookup, map)
5257import Data.Functor.Identity (Identity (..))
5358import Data.Typeable (Typeable)
5459import GHC.Exts (coerce)
55−import Type.Reflection (SomeTypeRep)
60+import Type.Reflection (SomeTypeRep, TypeRep)
5661
5762import qualified Data.TypeRepMap as F
5863
@@ -87,6 +92,7 @@ one x = coerce (F.one @a @Identity $ coerce x)
8792{- |
8893
8994Insert a value into a 'TMap'.
95+TMap optimizes for fast reads rather than inserts, as a trade-off inserts are @O(n)@.
9096
9197prop> size (insert v tm) >= size tm
9298prop> member @a (insert (x :: a) tm) == True
@@ -98,6 +104,9 @@ insert x = coerce (F.insert @a @Identity $ coerce x)
98104
99105{- | Delete a value from a 'TMap'.
100106
107+TMap optimizes for fast reads rather than modifications, as a trade-off deletes are @O(n)@,
108+with an @O(log(n))@ optimization for when the element is already missing.
109+
101110prop> size (delete @a tm) <= size tm
102111prop> member @a (delete @a tm) == False
103112
@@ -127,6 +136,24 @@ union :: TMap -> TMap -> TMap
127136union = F.union
128137{-# INLINE union #-}
129138
139+-- | The intersection of two 'TMap's using a combining function.
140+--
141+-- @O(n + m)@
142+intersectionWith :: (forall x. Typeable x => x -> x -> x) -> TMap -> TMap -> TMap
143+intersectionWith f = F.intersectionWith fId
144+ where
145+ fId :: forall y . Typeable y => Identity y -> Identity y -> Identity y
146+ fId y1 y2 = f (coerce y1) (coerce y2)
147+{-# INLINE intersectionWith #-}
148+
149+-- | The intersection of two 'TMap's.
150+-- It keeps all values from the first map whose keys are present in the second.
151+--
152+-- @O(n + m)@
153+intersection :: TMap -> TMap -> TMap
154+intersection = F.intersection
155+{-# INLINE intersection #-}
156+
130157{- | Lookup a value of the given type in a 'TMap'.
131158
132159>>> x = lookup $ insert (11 :: Int) empty
@@ -160,6 +187,16 @@ keys :: TMap -> [SomeTypeRep]
160187keys = F.keys
161188{-# INLINE keys #-}
162189
190+-- | Return the list of keys by wrapping them with a user-provided function.
191+keysWith :: (forall a. TypeRep a -> r) -> TMap -> [r]
192+keysWith = F.keysWith
193+{-# INLINE keysWith #-}
194+
195+-- | Return the list of key-value pairs by wrapping them with a user-provided function.
196+toListWith :: (forall a. Typeable a => a -> r) -> TMap -> [r]
197+toListWith = F.toListWith
198+{-# INLINE toListWith #-}
199+
163200-- | Map a function over the values.
164201map :: (forall a. Typeable a => a -> a) -> TMap -> TMap
165202map f = F.hoistWithKey (liftToIdentity f)
@@ -170,5 +207,14 @@ adjust :: Typeable a => (a -> a) -> TMap -> TMap
170207adjust f = F.adjust (liftToIdentity f)
171208{-# INLINE adjust #-}
172209
210+-- | Updates a value at a specific key, whether or not it exists.
211+-- This can be used to insert, delete, or update a value of a given type in the map.
212+alter :: Typeable a => (Maybe a -> Maybe a) -> TMap -> TMap
213+alter f = F.alter (liftF f)
214+ where
215+ liftF :: forall a. (Maybe a -> Maybe a) -> Maybe (Identity a) -> Maybe (Identity a)
216+ liftF = coerce
217+{-# INLINE alter #-}
218+
173219liftToIdentity :: forall a. (a -> a) -> Identity a -> Identity a
174220liftToIdentity = coerce
src/Data/TypeRepMap.hs+6−1
@@ -5,7 +5,7 @@ Copyright: (c) 2017-2020 Kowainik
55SPDX-License-Identifier: MPL-2.0
66Maintainer: Kowainik <xrom.xkov@gmail.com>
77
8− A version of 'Data.TMap.TMap' parametrized by an interpretation @f@. This
8+A version of 'Data.TMap.TMap' parametrized by an interpretation @f@. This
99sort of parametrization may be familiar to users of @vinyl@ records.
1010
1111@'TypeRepMap' f@ is a more efficient replacement for @DMap
@@ -57,17 +57,22 @@ module Data.TypeRepMap
5757 , insert
5858 , delete
5959 , adjust
60+ , alter
6061 , hoist
6162 , hoistA
6263 , hoistWithKey
6364 , unionWith
6465 , union
66+ , intersectionWith
67+ , intersection
6568
6669 -- * Query
6770 , lookup
6871 , member
6972 , size
7073 , keys
74+ , keysWith
75+ , toListWith
7176
7277 -- * 'IsList'
7378 , WrapTypeable (..)
src/Data/TypeRepMap/Internal.hs+196−49
@@ -45,9 +45,9 @@ import Data.Kind (Type)
4545import Data.Type.Equality ((:~:) (..), TestEquality (..))
4646import Data.List (intercalate, nubBy)
4747import Data.Maybe (fromMaybe)
48−import Data.Primitive.Array (Array, MutableArray, indexArray, mapArray', readArray, sizeofArray,
48+import Data.Primitive.Array (Array, MutableArray, indexArray, mapArray', sizeofArray,
4949 thawArray, unsafeFreezeArray, writeArray)
50−import Data.Primitive.PrimArray (PrimArray, indexPrimArray, sizeofPrimArray)
50+import Data.Primitive.PrimArray (writePrimArray, newPrimArray, unsafeFreezePrimArray, primArrayToList, MutablePrimArray, primArrayFromListN, PrimArray, indexPrimArray, sizeofPrimArray)
5151import Data.Semigroup (Semigroup (..), All(..))
5252import GHC.Base (Any, Int (..), Int#, (*#), (+#), (<#))
5353import GHC.Exts (IsList (..), inline, sortWith)
@@ -64,7 +64,6 @@ import Type.Reflection (SomeTypeRep (..), TypeRep, Typeable, typeRep, withTypeab
6464import Type.Reflection.Unsafe (typeRepFingerprint)
6565import Unsafe.Coerce (unsafeCoerce)
6666
67−import qualified Data.Map.Strict as Map
6867import qualified GHC.Exts as GHC (fromList, toList)
6968
7069{- |
@@ -170,33 +169,49 @@ prop> member @a (one (x :: f a)) == True
170169
171170-}
172171one :: forall a f . Typeable a => f a -> TypeRepMap f
173−one x = insert x empty
172+one x = TypeRepMap (primArrayFromListN 1 [fa])
173+ (primArrayFromListN 1 [fb])
174+ (pure @Array v)
175+ (pure @Array k)
176+ where
177+ (Fingerprint fa fb, v, k) = (calcFp @a, toAny x, unsafeCoerce $ typeRep @a)
174178{-# INLINE one #-}
175179
176180{- |
177181
178182Insert a value into a 'TypeRepMap'.
183+TypeRepMap optimizes for fast reads rather than inserts, as a trade-off inserts are @O(n)@.
179184
180185prop> size (insert v tm) >= size tm
181186prop> member @a (insert (x :: f a) tm) == True
182187
183188-}
184189insert :: forall a f . Typeable a => f a -> TypeRepMap f -> TypeRepMap f
185−insert x = fromTriples . addX . toTriples
190+insert x m
191+ | size m == 0 = one x
192+ | otherwise = case cachedBinarySearch (typeFp @a) (fingerprintAs m) (fingerprintBs m) of
193+ Nothing -> union m $ one x
194+ Just i -> m {trAnys = changeAnyArr i (trAnys m)}
186195 where
187− tripleX :: (Fingerprint, Any, Any)
188− tripleX@(fpX, _, _) = (calcFp @a, toAny x, unsafeCoerce $ typeRep @a)
189−
190− addX :: [(Fingerprint, Any, Any)] -> [(Fingerprint, Any, Any)]
191− addX l = tripleX : deleteByFst fpX l
196+ changeAnyArr :: Int -> Array Any -> Array Any
197+ changeAnyArr i trAs = runST $ do
198+ let n = sizeofArray trAs
199+ mutArr <- thawArray trAs 0 n
200+ writeArray mutArr i $ toAny x
201+ unsafeFreezeArray mutArr
192202{-# INLINE insert #-}
193203
194204-- Extract the kind of a type. We use it to work around lack of syntax for
195205-- inferred type variables (which are not subject to type applications).
196206type KindOf (a :: k) = k
197207
208+type ArgKindOf (f :: k -> l) = k
209+
198210{- | Delete a value from a 'TypeRepMap'.
199211
212+TypeRepMap optimizes for fast reads rather than modifications, as a trade-off deletes are
213+@O(n)@, with an @O(log(n))@ optimization for when the element is already missing.
214+
200215prop> size (delete @a tm) <= size tm
201216prop> member @a (delete @a tm) == False
202217
@@ -209,30 +224,60 @@ False
209224True
210225-}
211226delete :: forall a (f :: KindOf a -> Type) . Typeable a => TypeRepMap f -> TypeRepMap f
212−delete = fromTriples . deleteByFst (typeFp @a) . toTriples
227+delete m
228+ -- Lookups are fast, so check if we even have the element first.
229+ | not (member @a m) = m
230+ -- We know we have the element, If the map has exactly one element, we can return the empty map
231+ | size m == 1 = empty
232+ -- Otherwise, filter out the element in linear time.
233+ | otherwise = fromSortedTriples . deleteFirst ((== typeFp @a) . fst3) . toSortedTriples $ m
213234{-# INLINE delete #-}
214235
236+deleteFirst :: (a -> Bool) -> [a] -> [a]
237+deleteFirst _ [] = []
238+deleteFirst p (x : xs) = if p x then xs else x : deleteFirst p xs
239+
215240{- |
216−Update a value at a specific key with the result of the provided function. When
217−the key is not a member of the map, the original map is returned.
241+Update a value at a specific key with the result of the provided function.
242+When the key is not a member of the map, the original map is returned.
218243
219244>>> trmap = fromList @(TypeRepMap Identity) [WrapTypeable $ Identity "a"]
220245>>> lookup @String $ adjust (fmap (++ "ww")) trmap
221246Just (Identity "aww")
222247-}
223248adjust :: forall a f . Typeable a => (f a -> f a) -> TypeRepMap f -> TypeRepMap f
224−adjust fun tr = case cachedBinarySearch (typeFp @a) (fingerprintAs tr) (fingerprintBs tr) of
225− Nothing -> tr
226− Just i -> tr {trAnys = changeAnyArr i (trAnys tr)}
249+adjust fun = alter (fmap fun)
250+{-# INLINE adjust #-}
251+
252+{- |
253+Updates a value at a specific key, whether or not it exists.
254+This can be used to insert, delete, or update a value of a given type in the map.
255+
256+>>> func = (\case Nothing -> Just (Identity "new"); Just (Identity s) -> Just (Identity (reverse s)))
257+>>> lookup @String $ alter func empty
258+Just (Identity "new")
259+>>> trmap = fromList @(TypeRepMap Identity) [WrapTypeable $ Identity "helllo"]
260+>>> lookup @String $ alter func trmap
261+>>> Just (Identity "olleh")
262+-}
263+alter :: forall a f . Typeable a => (Maybe (f a) -> Maybe (f a)) -> TypeRepMap f -> TypeRepMap f
264+alter fun tr = case cachedBinarySearch (typeFp @a) (fingerprintAs tr) (fingerprintBs tr) of
265+ Nothing ->
266+ case (fun Nothing) of
267+ Nothing -> tr
268+ Just v -> insert v tr
269+ Just i ->
270+ case fun (Just . fromAny $ indexArray (trAnys tr) i) of
271+ Nothing -> delete @a tr
272+ Just v -> tr{trAnys = replaceAnyAt i (toAny v) (trAnys tr)}
227273 where
228− changeAnyArr :: Int -> Array Any -> Array Any
229− changeAnyArr i trAs = runST $ do
274+ replaceAnyAt :: Int -> Any -> Array Any -> Array Any
275+ replaceAnyAt i v trAs = runST $ do
230276 let n = sizeofArray trAs
231277 mutArr <- thawArray trAs 0 n
232− a <- toAny . fun . fromAny <$> readArray mutArr i
233− writeArray mutArr i a
278+ writeArray mutArr i v
234279 unsafeFreezeArray mutArr
235−{-# INLINE adjust #-}
280+{-# INLINE alter #-}
236281
237282{- | Map over the elements of a 'TypeRepMap'.
238283
@@ -266,33 +311,89 @@ hoistWithKey f (TypeRepMap as bs ans ks) = TypeRepMap as bs newAns ks
266311 withTr t = withTypeable t f
267312{-# INLINE hoistWithKey #-}
268313
269−-- | The union of two 'TypeRepMap's using a combining function.
314+-- | The union of two 'TypeRepMap's using a combining function for conflicting entries. @O(n + m)@
270315unionWith :: forall f. (forall x. Typeable x => f x -> f x -> f x) -> TypeRepMap f -> TypeRepMap f -> TypeRepMap f
271−unionWith f m1 m2 = fromTriples
272− $ toTripleList
273− $ Map.unionWith combine
274− (fromTripleList $ toTriples m1)
275− (fromTripleList $ toTriples m2)
316+unionWith f ma mb = do
317+ fromSortedTriples $ mergeMaps (toSortedTriples ma) (toSortedTriples mb)
276318 where
277319 f' :: forall x. TypeRep x -> f x -> f x -> f x
278320 f' tr = withTypeable tr f
279321
280− combine :: (Any, Any) -> (Any, Any) -> (Any, Any)
281− combine (av, ak) (bv, _) = (toAny $ f' (fromAny ak) (fromAny av) (fromAny bv), ak)
282−
283− fromTripleList :: Ord a => [(a, b, c)] -> Map.Map a (b, c)
284− fromTripleList = Map.fromList . map (\(a, b, c) -> (a, (b, c)))
285−
286− toTripleList :: Map.Map a (b, c) -> [(a, b, c)]
287− toTripleList = map (\(a, (b, c)) -> (a, b, c)) . Map.toList
322+ combine :: (Fingerprint, Any, Any) -> (Fingerprint, Any, Any) -> (Fingerprint, Any, Any)
323+ combine (fp, av, ak) (_, bv, _) = (fp, toAny $ f' (fromAny ak) (fromAny av) (fromAny bv), ak)
324+
325+ -- Merges two typrepmaps into a sorted, dedup'd list of triples.
326+ -- Using 'toSortedTriples' allows us to assume the triples are sorted by fingerprint,
327+ -- Given O(n) performance from 'toSortedTriples', and given that we can merge-sort in
328+ -- O(n + m) time, then can '.fromSortedTriples' back into cachedBinarySearch order in O(n + m)
329+ -- that gives a total of O(n + m).
330+ mergeMaps :: [(Fingerprint, Any, Any)] -> [(Fingerprint, Any, Any)] -> [(Fingerprint, Any, Any)]
331+ -- We've addressed all elements from both maps
332+ mergeMaps as [] = as
333+ mergeMaps [] bs = bs
334+ -- Merge
335+ mergeMaps (a@(af, _, _) : as) (b@(bf, _, _) : bs) =
336+ case compare af bf of
337+ -- Fingerprints are equal, union the elements using our function
338+ -- If the incoming maps were de-duped, there shouldn't be any other equivalent
339+ -- fingerprints
340+ EQ -> combine a b : mergeMaps as bs
341+ -- First fingerprint must not be in the second map or we would have seen it by now
342+ -- Add it to the result as-is
343+ LT -> a : mergeMaps as (b : bs)
344+ -- Second fingerprint must not be in the first map or we would have seen it by now
345+ -- Add it to the result as-is
346+ GT -> b : mergeMaps (a:as) bs
288347{-# INLINE unionWith #-}
289348
290−-- | The (left-biased) union of two 'TypeRepMap's. It prefers the first map when
349+-- | The (left-biased) union of two 'TypeRepMap's in @O(n + m)@. It prefers the first map when
291350-- duplicate keys are encountered, i.e. @'union' == 'unionWith' const@.
292351union :: TypeRepMap f -> TypeRepMap f -> TypeRepMap f
293352union = unionWith const
294353{-# INLINE union #-}
295354
355+-- | The 'intersection' of two 'TypeRepMap's using a combining function
356+--
357+-- @O(n + m)@
358+intersectionWith :: forall f. (forall x. Typeable x => f x -> f x -> f x) -> TypeRepMap f -> TypeRepMap f -> TypeRepMap f
359+intersectionWith f ma mb =
360+ fromSortedTriples $ mergeMaps (toSortedTriples ma) (toSortedTriples mb)
361+ where
362+ f' :: forall x. TypeRep x -> f x -> f x -> f x
363+ f' tr = withTypeable tr f
364+
365+ combine :: (Fingerprint, Any, Any) -> (Fingerprint, Any, Any) -> (Fingerprint, Any, Any)
366+ combine (fp, av, ak) (_, bv, _) = (fp, toAny $ f' (fromAny ak) (fromAny av) (fromAny bv), ak)
367+
368+ -- Merges two typrepmaps into a sorted, dedup'd list of triples.
369+ mergeMaps :: [(Fingerprint, Any, Any)] -> [(Fingerprint, Any, Any)] -> [(Fingerprint, Any, Any)]
370+ -- If either list is empty, the intersection must be finished.
371+ mergeMaps _ [] = []
372+ mergeMaps [] _ = []
373+ -- Merge the two maps considering one element at a time.
374+ mergeMaps (a@(af, _, _) : as) (b@(bf, _, _) : bs) =
375+ case compare af bf of
376+ -- Fingerprints are equal, union the elements using our function
377+ -- If the incoming maps were de-duped, there shouldn't be any other equivalent
378+ -- fingerprints
379+ EQ -> combine a b : mergeMaps as bs
380+ -- First fingerprint must not be in the second map or we would have seen it by now
381+ -- Skip it an move on
382+ LT -> mergeMaps as (b : bs)
383+ -- Second fingerprint must not be in the first map or we would have seen it by now
384+ -- Skip it an move on
385+ GT -> mergeMaps (a:as) bs
386+{-# INLINE intersectionWith #-}
387+
388+-- | The intersection of two 'TypeRepMap's.
389+-- It keeps all values from the first map whose keys are present in the second.
390+--
391+-- @O(n + m)@
392+intersection :: TypeRepMap f -> TypeRepMap f -> TypeRepMap f
393+intersection = intersectionWith const
394+{-# INLINE intersection #-}
395+
396+
296397{- | Check if a value of the given type is present in a 'TypeRepMap'.
297398
298399>>> member @Char $ one (Identity 'a')
@@ -328,9 +429,22 @@ size = sizeofPrimArray . fingerprintAs
328429
329430-- | Return the list of 'SomeTypeRep' from the keys.
330431keys :: TypeRepMap f -> [SomeTypeRep]
331−keys TypeRepMap{..} = SomeTypeRep . anyToTypeRep <$> toList trKeys
432+keys = keysWith SomeTypeRep
332433{-# INLINE keys #-}
333434
435+-- | Return the list of keys by wrapping them with a user-provided function.
436+keysWith :: (forall (a :: ArgKindOf f). TypeRep a -> r) -> TypeRepMap f -> [r]
437+keysWith f TypeRepMap{..} = f . anyToTypeRep <$> toList trKeys
438+{-# INLINE keysWith #-}
439+
440+-- | Return the list of key-value pairs by wrapping them with a user-provided function.
441+toListWith :: forall f r. (forall (a :: ArgKindOf f). Typeable a => f a -> r) -> TypeRepMap f -> [r]
442+toListWith f = map toF . toTriples
443+ where
444+ withTypeRep :: TypeRep a -> f a -> r
445+ withTypeRep tr an = withTypeable tr $ f an
446+ toF (_, an, k) = withTypeRep (unsafeCoerce k) (fromAny an)
447+
334448-- | Binary searched based on this article
335449-- http://bannalia.blogspot.com/2015/06/cache-friendly-binary-search.html
336450-- with modification for our two-vector search case.
@@ -374,8 +488,20 @@ typeFp = typeRepFingerprint $ typeRep @a
374488toTriples :: TypeRepMap f -> [(Fingerprint, Any, Any)]
375489toTriples tm = zip3 (toFingerprints tm) (GHC.toList $ trAnys tm) (GHC.toList $ trKeys tm)
376490
377−deleteByFst :: Eq a => a -> [(a, b, c)] -> [(a, b, c)]
378−deleteByFst x = filter ((/= x) . fst3)
491+-- | Efficiently get sorted triples from a map in O(n) time
492+--
493+-- We assume the incoming TypeRepMap is already sorted into 'cachedBinarySearch' order using fromSortedList.
494+-- Then we can construct the index mapping from the "cached" ordering into monotonically
495+-- increasing order using 'generateOrderMapping' with the length of the TRM. This takes @O(n).
496+-- We then pull those indexes from the source TRM to get the sorted triples in a total of @O(n).
497+toSortedTriples :: TypeRepMap f -> [(Fingerprint, Any, Any)]
498+toSortedTriples tm = trip <$> ordering
499+ where
500+ trip i = ( Fingerprint (indexPrimArray (fingerprintAs tm) i) (indexPrimArray (fingerprintBs tm) i)
501+ , indexArray (trAnys tm) i
502+ , indexArray (trKeys tm) i)
503+ ordering :: [ Int ]
504+ ordering = generateOrderMapping (size tm)
379505
380506nubByFst :: (Eq a) => [(a, b, c)] -> [(a, b, c)]
381507nubByFst = nubBy ((==) `on` fst3)
@@ -395,9 +521,6 @@ data WrapTypeable f where
395521instance Show (WrapTypeable f) where
396522 show (WrapTypeable (_ :: f a)) = show $ calcFp @a
397523
398−wrapTypeable :: TypeRep a -> f a -> WrapTypeable f
399−wrapTypeable tr = withTypeable tr WrapTypeable
400−
401524{- |
402525
403526prop> fromList . toList == 'id'
@@ -425,19 +548,19 @@ instance IsList (TypeRepMap f) where
425548 k (WrapTypeable (_ :: f a)) = unsafeCoerce $ typeRep @a
426549
427550 toList :: TypeRepMap f -> [WrapTypeable f]
428− toList = map toWrapTypeable . toTriples
429− where
430− toWrapTypeable :: (Fingerprint, Any, Any) -> WrapTypeable f
431− toWrapTypeable (_, an, k) = wrapTypeable (unsafeCoerce k) (fromAny an)
551+ toList = toListWith WrapTypeable
432552
433553calcFp :: forall a . Typeable a => Fingerprint
434554calcFp = typeRepFingerprint $ typeRep @a
435555
436556fromTriples :: [(Fingerprint, Any, Any)] -> TypeRepMap f
437−fromTriples kvs = TypeRepMap (GHC.fromList fpAs) (GHC.fromList fpBs) (GHC.fromList ans) (GHC.fromList ks)
557+fromTriples = fromSortedTriples . sortWith fst3 . nubByFst
558+
559+fromSortedTriples :: [(Fingerprint, Any, Any)] -> TypeRepMap f
560+fromSortedTriples kvs = TypeRepMap (GHC.fromList fpAs) (GHC.fromList fpBs) (GHC.fromList ans) (GHC.fromList ks)
438561 where
439562 (fpAs, fpBs) = unzip $ map (\(Fingerprint a b) -> (a, b)) fps
440− (fps, ans, ks) = unzip3 $ fromSortedList $ sortWith fst3 $ nubByFst kvs
563+ (fps, ans, ks) = unzip3 $ fromSortedList kvs
441564
442565----------------------------------------------------------------------------
443566-- Tree-like conversion
@@ -464,11 +587,35 @@ fromSortedList l = runST $ do
464587 writeArray result i (indexArray origin newFirst)
465588 loop (2 * i + 2) (newFirst + 1)
466589
590+-- Returns a list of indexes which represents the "sorted" order of an array generated by
591+-- fromSortedList of the provided length.
592+-- I.e. fmap (fromSortedList [1, 2, 3, 4, 5, 6] !!) (generateOrderMapping 6) == [1, 2, 3, 4, 5, 6]
593+--
594+-- >>> generateOrderMapping 6
595+-- [3,1,4,0,5,2]
596+--
597+-- >>> generateOrderMapping 8
598+-- [7,3,1,4,0,5,2,6]
599+generateOrderMapping :: Int -> [Int]
600+generateOrderMapping len = runST $ do
601+ orderMappingArr <- newPrimArray len
602+ _ <- loop orderMappingArr 0 0
603+ primArrayToList <$> unsafeFreezePrimArray orderMappingArr
604+ where
605+ loop :: MutablePrimArray s Int -> Int -> Int -> ST s Int
606+ loop result i first =
607+ if i >= len
608+ then pure first
609+ else do
610+ newFirst <- loop result (2 * i + 1) first
611+ writePrimArray result newFirst i
612+ loop result (2 * i + 2) (newFirst + 1)
613+
467614----------------------------------------------------------------------------
468615-- Helper functions.
469616----------------------------------------------------------------------------
470617
471−-- | Check that invariant of the structure is hold.
618+-- | Check that invariant of the structure holds.
472619-- The structure maintains the following invariant.
473620-- For each element @A@ at index @i@:
474621--
test/Test/TypeRep/TypeRepMap.hs+62−4
@@ -5,14 +5,18 @@ module Test.TypeRep.TypeRepMap
55import Prelude hiding (lookup)
66
77import Data.Functor.Identity (Identity (..))
8−import GHC.Exts (fromList)
9−import Test.Hspec (Spec, describe, it, shouldBe)
8+import Data.List (sortBy)
9+import Data.Ord (comparing)
10+import Data.Typeable (cast)
11+import GHC.Exts (fromList, toList)
12+import Type.Reflection (SomeTypeRep(..), typeRep)
13+import Test.Hspec (Spec, describe, it, shouldBe, shouldMatchList, shouldSatisfy, expectationFailure)
1014
11−import Data.TMap (TMap, empty, insert, lookup, one, size, union)
15+import Data.TMap (TMap, empty, insert, lookup, one, size, union, keys)
1216import Data.TypeRepMap.Internal (WrapTypeable (..))
1317
1418
15−-- Simple test for 'lookup', 'insert' and 'size' functions.
19+-- Simple test for 'lookup', 'insert', 'size', 'keys', 'toList' functions.
1620typeRepMapSpec :: Spec
1721typeRepMapSpec = describe "TypeRepMap" $ do
1822 describe "Lookup Test" $ do
@@ -39,6 +43,60 @@ typeRepMapSpec = describe "TypeRepMap" $ do
3943 lookup m `shouldBe` Just True
4044 lookup @Int m `shouldBe` Nothing
4145
46+ describe "Keys Test" $ do
47+ it "returns nothing on empty map" $
48+ keys empty `shouldBe` []
49+ it "returns the correct TypeRep" $
50+ keys (one 'a') `shouldBe` [SomeTypeRep $ typeRep @Char]
51+ it "returns the correct TypeReps for 10 different types" $
52+ keys mapOf10 `shouldMatchList`
53+ [ SomeTypeRep $ typeRep @Bool
54+ , SomeTypeRep $ typeRep @[Bool]
55+ , SomeTypeRep $ typeRep @(Maybe Bool)
56+ , SomeTypeRep $ typeRep @(Maybe ())
57+ , SomeTypeRep $ typeRep @[()]
58+ , SomeTypeRep $ typeRep @()
59+ , SomeTypeRep $ typeRep @String
60+ , SomeTypeRep $ typeRep @(Maybe Char)
61+ , SomeTypeRep $ typeRep @Char
62+ , SomeTypeRep $ typeRep @Int
63+ ]
64+
65+ describe "ToList Test" $ do
66+ it "returns nothing on empty map" $
67+ toList empty `shouldSatisfy` null
68+ it "returns correct result when 1 element is inserted" $
69+ case toList (one 'a') of
70+ [WrapTypeable (Identity x)] -> cast x `shouldBe` Just 'a'
71+ _ -> expectationFailure "did not return exactly 1 result"
72+ it "returns correct result when 10 elements are inserted" $ do
73+ let
74+ getTypeRep (WrapTypeable (Identity (_ :: a))) = SomeTypeRep $ typeRep @a
75+ got = sortBy (comparing getTypeRep) (toList mapOf10)
76+ expected = sortBy (comparing fst)
77+ [ (SomeTypeRep $ typeRep @Bool,
78+ \(WrapTypeable (Identity x)) -> cast x `shouldBe` Just True)
79+ , (SomeTypeRep $ typeRep @[Bool],
80+ \(WrapTypeable (Identity x)) -> cast x `shouldBe` Just [True, False])
81+ , (SomeTypeRep $ typeRep @(Maybe Bool),
82+ \(WrapTypeable (Identity x)) -> cast x `shouldBe` Just (Just True))
83+ , (SomeTypeRep $ typeRep @(Maybe ()),
84+ \(WrapTypeable (Identity x)) -> cast x `shouldBe` Just (Just ()))
85+ , (SomeTypeRep $ typeRep @[()],
86+ \(WrapTypeable (Identity x)) -> cast x `shouldBe` Just [()])
87+ , (SomeTypeRep $ typeRep @(),
88+ \(WrapTypeable (Identity x)) -> cast x `shouldBe` Just ())
89+ , (SomeTypeRep $ typeRep @String,
90+ \(WrapTypeable (Identity x)) -> cast x `shouldBe` Just ("aaa" :: String))
91+ , (SomeTypeRep $ typeRep @(Maybe Char),
92+ \(WrapTypeable (Identity x)) -> cast x `shouldBe` Just (Just 'a'))
93+ , (SomeTypeRep $ typeRep @Char,
94+ \(WrapTypeable (Identity x)) -> cast x `shouldBe` Just 'a')
95+ , (SomeTypeRep $ typeRep @Int,
96+ \(WrapTypeable (Identity x)) -> cast x `shouldBe` Just (11 :: Int))
97+ ]
98+ length got `shouldBe` 10
99+ sequence_ $ zipWith snd expected got
42100
43101mapOf10 :: TMap
44102mapOf10 = insert True
test/Test/TypeRep/TypeRepMapProperty.hs+69−26
@@ -20,7 +20,8 @@ import Test.Hspec (Arg, Expectation, Spec, SpecWith, describe, it)
2020import Test.Hspec.Hedgehog (hedgehog)
2121
2222import Data.TypeRepMap.Internal (TypeRepMap (..), WrapTypeable (..), delete, insert, invariantCheck,
23− lookup, member)
23+ lookup, member, union, generateOrderMapping, fromSortedList,
24+ adjust, alter, intersection)
2425
2526import qualified Hedgehog.Gen as Gen
2627import qualified Hedgehog.Range as Range
@@ -34,6 +35,13 @@ typeRepMapPropertySpec = describe "TypeRepMap Property tests" $ do
3435 deleteMemberSpec
3536 insertInvariantSpec
3637 deleteInvariantSpec
38+ alterInsertSpec
39+ alterDeleteSpec
40+ alterAdjustSpec
41+ alterModifySpec
42+ intersectionSpec
43+ describe "Internal helpers" $ do
44+ generateOrderMappingInvariantSpec
3745 describe "Instance Laws" $ do
3846 semigroupAssocSpec
3947 monoidIdentitySpec
@@ -80,45 +88,66 @@ deleteInvariantSpec = it "invariantCheck (delete k b) == True" $ hedgehog $ do
8088 WrapTypeable (_ :: IntProxy n) <- forAll genTF
8189 assert $ invariantCheck (delete @n m)
8290
91+alterInsertSpec :: Property
92+alterInsertSpec = it "insert proxy m === alter (const (Just proxy)) m" $ hedgehog $ do
93+ m <- forAll genMap
94+ WrapTypeable (proxy :: IntProxy n) <- forAll genTF
95+ insert proxy m === alter (const (Just proxy)) m
96+
97+alterDeleteSpec :: Property
98+alterDeleteSpec = it "delete proxy m === alter (const Nothing) m" $ hedgehog $ do
99+ WrapTypeable (proxy :: IntProxy n) <- forAll genTF
100+ m <- insert proxy <$> forAll genMap
101+ delete @n @IntProxy m === alter @n @IntProxy (const Nothing) m
102+
103+alterAdjustSpec :: Property
104+alterAdjustSpec = it "adjust f m == alter (fmap f) m" $ hedgehog $ do
105+ m <- forAll genMap
106+ WrapTypeable (_ :: IntProxy n) <- forAll genTF
107+ let f (IntProxy p n) = IntProxy p (n * 10)
108+ adjust @n @IntProxy f m === alter @n @IntProxy (fmap f) m
109+
110+alterModifySpec :: Property
111+alterModifySpec = it "lookup k (alter f) == f (lookup k m)" $ hedgehog $ do
112+ m <- forAll genMap
113+ WrapTypeable (_ :: IntProxy n) <- forAll genTF
114+ randInt <- forAll (Gen.int Range.constantBounded)
115+ -- Function with some interesting behaviour, which inserts, seletes and modifies
116+ let f Nothing = Just (IntProxy Proxy randInt)
117+ f (Just (IntProxy p n))
118+ | even n = Nothing
119+ | otherwise = Just $ IntProxy p (n * 10)
120+ lookup @n @IntProxy (alter @n f m) === f (lookup @n @IntProxy m)
121+
122+intersectionSpec :: Property
123+intersectionSpec = it "m `intersection` (m `union` n) == m" $ hedgehog $ do
124+ m <- forAll genMap
125+ n <- forAll genMap
126+ m `intersection` (m `union` n) === m
127+
83128----------------------------------------------------------------------------
84−-- Semigroup and Monoid laws
129+-- Internal helpers
85130----------------------------------------------------------------------------
131+generateOrderMappingInvariantSpec :: Property
132+generateOrderMappingInvariantSpec =
133+ it "fmap (fromSortedList [1 .. n] !!) (generateOrderMapping n) == [1 .. n]" $ hedgehog $ do
134+ n <- forAll $ Gen.int (Range.linear 0 100)
135+ fmap (fromSortedList [1 .. n] !!) (generateOrderMapping n) === [1 .. n]
86136
87−#if __GLASGOW_HASKELL__ < 806
88−{- | This newtype is used to compare 'TypeRepMap's using only 'Fingerprint's.
89−It's not a good idea to write such 'Eq' instance for 'TypeRepMap' itself because
90−it doesn't compare values so it's not true equality. But this should be enough
91−for tests.
92−-}
93−newtype FpMap f = FpMap (TypeRepMap f)
94− deriving newtype (Show, Semigroup, Monoid)
95−
96−instance Eq (FpMap f) where
97− FpMap (TypeRepMap as1 bs1 _ _) == FpMap (TypeRepMap as2 bs2 _ _) =
98− as1 == as2 && bs1 == bs2
99−#endif
137+----------------------------------------------------------------------------
138+-- Semigroup and Monoid laws
139+----------------------------------------------------------------------------
100140
101141semigroupAssocSpec :: Property
102142semigroupAssocSpec = it "x <> (y <> z) == (x <> y) <> z" $ hedgehog $ do
103−#if __GLASGOW_HASKELL__ >= 806
104143 x <- forAll genMap
105144 y <- forAll genMap
106145 z <- forAll genMap
107−#else
108− x <- FpMap <$> forAll genMap
109− y <- FpMap <$> forAll genMap
110− z <- FpMap <$> forAll genMap
111−#endif
112146 (x <> (y <> z)) === ((x <> y) <> z)
113147
114148monoidIdentitySpec :: Property
115149monoidIdentitySpec = it "x <> mempty == mempty <> x == x" $ hedgehog $ do
116−#if __GLASGOW_HASKELL__ >= 806
117150 x <- forAll genMap
118−#else
119− x <- FpMap <$> forAll genMap
120−#endif
121−
122151 x <> mempty === x
123152 mempty <> x === x
124153
@@ -139,3 +168,17 @@ genTF = do
139168 case someNatVal randNat of
140169 Just (SomeNat proxyNat) -> pure $ WrapTypeable $ IntProxy proxyNat randInt
141170 Nothing -> error "Invalid test generator"
171+
172+----------------------------------------------------------------------------
173+-- Helpers
174+----------------------------------------------------------------------------
175+#if __GLASGOW_HASKELL__ < 806
176+{- | We add an orphan Eq instance for old GHC versions just to make testing easier.
177+It's not a good idea to write such 'Eq' instance for 'TypeRepMap' itself because
178+it doesn't compare values so it's not true equality. But this should be enough
179+for tests.
180+-}
181+instance Eq (TypeRepMap f) where
182+ TypeRepMap as1 bs1 _ _ == TypeRepMap as2 bs2 _ _ =
183+ as1 == as2 && bs1 == bs2
184+#endif
typerep-map.cabal+1−2
@@ -73,8 +73,7 @@ library
7373 Data.TypeRepMap
7474 Data.TypeRepMap.Internal
7575
76− build-depends: containers >= 0.5.10.2 && < 0.7
77− , ghc-prim >= 0.5.1.1 && < 0.7
76+ build-depends: ghc-prim >= 0.5.1.1 && < 0.7
7877 , primitive ^>= 0.7.0
7978 , deepseq ^>= 1.4
8079