disjoint-containers 0.2.3 → 0.2.4
raw patch · 4 files changed
+227/−72 lines, 4 filesdep +enum-typesdep +quickcheck-enum-instancesdep +tastydep ~QuickCheckdep ~aesondep ~basePVP: major bump suggested
API removals or changes: PVP suggests a major version bump
Dependencies added: enum-types, quickcheck-enum-instances, tasty, tasty-quickcheck
Dependency ranges changed: QuickCheck, aeson, base, containers, quickcheck-classes
API changes (from Hackage documentation)
- Data.DisjointMap: instance (GHC.Classes.Ord k, GHC.Base.Monoid v) => Data.Semigroup.Semigroup (Data.DisjointMap.DisjointMap k v)
- Data.DisjointSet: instance GHC.Classes.Ord a => Data.Semigroup.Semigroup (Data.DisjointSet.DisjointSet a)
+ Data.DisjointMap: fromSets :: Ord k => [(Set k, v)] -> Maybe (DisjointMap k v)
+ Data.DisjointMap: instance (GHC.Classes.Ord k, GHC.Base.Semigroup v) => GHC.Base.Semigroup (Data.DisjointMap.DisjointMap k v)
+ Data.DisjointMap: toSets :: DisjointMap k v -> [(Set k, v)]
+ Data.DisjointMap: unionWeakly :: (Ord k, Semigroup v) => k -> k -> DisjointMap k v -> DisjointMap k v
+ Data.DisjointSet: instance GHC.Classes.Ord a => GHC.Base.Semigroup (Data.DisjointSet.DisjointSet a)
- Data.DisjointMap: insert :: (Ord k, Monoid v) => k -> v -> DisjointMap k v -> DisjointMap k v
+ Data.DisjointMap: insert :: (Ord k, Semigroup v) => k -> v -> DisjointMap k v -> DisjointMap k v
Files
- README.md +12/−0
- disjoint-containers.cabal +10/−6
- src/Data/DisjointMap.hs +113/−38
- test/Spec.hs +92/−28
README.md view
@@ -1,1 +1,13 @@ # disjoint-containers++`disjoint-containers` has two new data structures: `DisjointSet` and `DisjointMap`++`DisjointSet`:++Disjoint-sets are a set of elements +with equivalence relations defined between elements, i.e. +two elements may be members of the same equivalence set.++`DisjointMap`:++Maps with disjoint sets as the key.
disjoint-containers.cabal view
@@ -1,5 +1,5 @@ name: disjoint-containers-version: 0.2.3+version: 0.2.4 synopsis: Disjoint containers description: Disjoint containers homepage: https://github.com/andrewthad/disjoint-containers#readme@@ -19,10 +19,10 @@ Data.DisjointSet Data.DisjointMap build-depends:- base >= 4.7 && < 5+ base >= 4.11.1 && < 5 , transformers >= 0.5 && < 0.6- , containers >= 0.5 && < 0.6- , aeson >= 0.11 && < 1.3+ , containers >= 0.5 && < 0.7+ , aeson >= 0.11 && < 1.5 default-language: Haskell2010 test-suite test@@ -33,8 +33,12 @@ base , disjoint-containers , containers- , QuickCheck- , quickcheck-classes >= 0.1 && < 0.3+ , QuickCheck >= 2.11+ , quickcheck-classes >= 0.4.14.1+ , tasty+ , tasty-quickcheck+ , enum-types+ , quickcheck-enum-instances , aeson , semigroups default-language: Haskell2010
src/Data/DisjointMap.hs view
@@ -29,6 +29,7 @@ , singletons , insert , union+ , unionWeakly -- * Query , lookup , lookup'@@ -36,6 +37,8 @@ , representative' -- * Conversion , toLists+ , toSets+ , fromSets , pretty , prettyList , foldlWithKeys'@@ -60,6 +63,7 @@ import qualified Data.Map.Strict as M import qualified Data.Set as S import qualified GHC.OldList as L+import qualified Data.Foldable as F -- | A map having disjoints sets of @k@ as keys and -- @v@ as values.@@ -74,10 +78,11 @@ deriving (Functor,Foldable,Traversable) instance (Ord k, Monoid v) => Monoid (DisjointMap k v) where- mappend = append mempty = empty -instance (Ord k, Monoid v) => SG.Semigroup (DisjointMap k v) where+-- | This only satisfies the associativity law when the 'Monoid'+-- instance for @v@ is commutative.+instance (Ord k, Semigroup v) => SG.Semigroup (DisjointMap k v) where (<>) = append -- technically, it should be possible to weaken the Ord constraint on v to@@ -151,7 +156,7 @@ {-| Create an equivalence relation between x and y. If either x or y-are not already is the disjoint set, they are first created+are not already in the disjoint set, they are first created as singletons with a value that is 'mempty'. -} union :: (Ord k, Monoid v) => k -> k -> DisjointMap k v -> DisjointMap k v@@ -174,6 +179,50 @@ EQ -> let p' = M.insert repx repy p r' = M.delete repx $! M.insert repy (Ranked (ranky + 1) keys val) r in DisjointMap p' r'+{-|+Create an equivalence relation between x and y. If both x and y+are missing, do not create either of them. Otherwise, they will+both exist in the map.+-}+unionWeakly :: (Ord k, Semigroup v) => k -> k -> DisjointMap k v -> DisjointMap k v+unionWeakly !x !y set = flip execState set $ runMaybeT $ do+ mx <- lift $ state $ lookupCompress x+ my <- lift $ state $ lookupCompress y+ case mx of+ Nothing -> case my of+ Nothing -> pure ()+ Just repy -> do+ DisjointMap p r <- lift get+ lift $ put $+ let p' = M.insert x repy p+ Ranked ranky keys val = fromMaybe (error "Data.DisjointMap.unionWeakly") (M.lookup repy r)+ r' = M.insert repy (Ranked ranky (S.insert x keys) val) r+ in DisjointMap p' r'+ Just repx -> case my of+ Nothing -> do+ DisjointMap p r <- lift get+ lift $ put $+ let p' = M.insert y repx p+ Ranked rankx keys val = fromMaybe (error "Data.DisjointMap.unionWeakly") (M.lookup repx r)+ r' = M.insert repx (Ranked rankx (S.insert y keys) val) r+ in DisjointMap p' r'+ Just repy -> do+ guard $ repx /= repy+ DisjointMap p r <- lift get+ let Ranked rankx keysx valx = r M.! repx+ let Ranked ranky keysy valy = r M.! repy+ let val = valx <> valy+ let keys = mappend keysx keysy+ lift $ put $! case compare rankx ranky of+ LT -> let p' = M.insert repx repy p+ r' = M.delete repx $! M.insert repy (Ranked ranky keys val) r+ in DisjointMap p' r'+ GT -> let p' = M.insert repy repx p+ r' = M.delete repy $! M.insert repx (Ranked rankx keys val) r+ in DisjointMap p' r'+ EQ -> let p' = M.insert repx repy p+ r' = M.delete repx $! M.insert repy (Ranked (ranky + 1) keys val) r+ in DisjointMap p' r' {-| Find the set representative for this input. This function@@ -184,14 +233,18 @@ {-| Insert a key-value pair into the disjoint map. If the key is is already present in another set, combine the value- monoidally with the value belonging to it.+ monoidally with the value belonging to it. The new value+ is on the left side of the append, and the old value is+ on the right. Otherwise, this creates a singleton set as a new key and associates it with the value. -}-insert :: (Ord k, Monoid v) => k -> v -> DisjointMap k v -> DisjointMap k v+insert :: (Ord k, Semigroup v) => k -> v -> DisjointMap k v -> DisjointMap k v insert !x = insertInternal x (S.singleton x) -insertInternal :: (Ord k, Monoid v) => k -> Set k -> v -> DisjointMap k v -> DisjointMap k v+-- Precondition: Nothing in ks already exists in the disjoint map.+-- This function should only be used by insert.+insertInternal :: (Ord k, Semigroup v) => k -> Set k -> v -> DisjointMap k v -> DisjointMap k v insertInternal !x !ks !v set@(DisjointMap p r) = let (l, p') = M.insertLookupWithKey (\_ _ old -> old) x x p in case l of@@ -199,7 +252,7 @@ let (m,DisjointMap p2 r') = representative' x set in case m of Nothing -> error "DisjointMap insert: invariant violated"- Just root -> DisjointMap p2 (M.adjust (\(Ranked rank oldKs vOld) -> Ranked rank (mappend oldKs ks) (mappend v vOld)) root r')+ Just root -> DisjointMap p2 (M.adjust (\(Ranked rank oldKs vOld) -> Ranked rank (mappend oldKs ks) (v <> vOld)) root r') Nothing -> let r' = M.insert x (Ranked 0 ks v) r in DisjointMap p' r'@@ -216,18 +269,27 @@ empty :: DisjointMap k v empty = DisjointMap M.empty M.empty -append :: (Ord k, Monoid v) => DisjointMap k v -> DisjointMap k v -> DisjointMap k v+append :: (Ord k, Semigroup v) => DisjointMap k v -> DisjointMap k v -> DisjointMap k v append s1@(DisjointMap m1 r1) s2@(DisjointMap m2 r2) = if M.size m1 > M.size m2- then appendParents r2 s1 m2- else appendParents r1 s2 m1+ then appendPhase2 (appendPhase1 r2 s1 m2) m2+ else appendPhase2 (appendPhase1 r1 s2 m1) m1 -appendParents :: (Ord k, Monoid v) => Map k (Ranked k v) -> DisjointMap k v -> Map k k -> DisjointMap k v-appendParents !ranks = M.foldlWithKey' $ \ds x y -> if x == y+appendPhase1 :: (Ord k, Semigroup v)+ => Map k (Ranked k v)+ -> DisjointMap k v+ -> Map k k+ -> DisjointMap k v+appendPhase1 !ranks = M.foldlWithKey' $ \ds x y -> if x == y then case M.lookup x ranks of- Nothing -> error "DisjointMap appendParents: invariant violated"- Just (Ranked _ ks v) -> insertInternal x ks v ds- else union x y ds+ Nothing -> error "Data.DisjointMap.appendParents: invariant violated"+ Just (Ranked _ ks v) -> F.foldl' (\dm k -> unionWeakly k x dm) (insert x v ds) ks+ else ds +appendPhase2 :: (Ord k, Semigroup v) => DisjointMap k v -> Map k k -> DisjointMap k v+appendPhase2 = M.foldlWithKey' $ \ds x y -> if x == y+ then ds+ else unionWeakly x y ds+ {-| Create a disjoint map with one key. Everything in the 'Set' argument is consider part of the same equivalence class.@@ -263,15 +325,25 @@ lookupCompressAdd !x set = case find x set of Nothing -> (x, insert x mempty set)- Just rep -> let set' = compress rep x set- in set' `seq` (rep, set')+ Just rep -> let !set' = compress rep x set+ in (rep, set') +lookupCompress :: Ord k => k -> DisjointMap k v -> (Maybe k, DisjointMap k v)+lookupCompress !x set =+ case find x set of+ Nothing -> (Nothing, set)+ Just rep ->+ let !set' = compress rep x set+ in (Just rep, set')+ find :: Ord k => k -> DisjointMap k v -> Maybe k-find !x (DisjointMap p _) =- do x' <- M.lookup x p- return $! if x == x' then x' else find' x'- where find' y = let y' = p M.! y- in if y == y' then y' else find' y'+find !x (DisjointMap p _) = do+ x' <- M.lookup x p+ return $! if x == x' then x' else find' x'+ where+ find' y =+ let y' = p M.! y+ in if y == y' then y' else find' y' {-| Find the value associated with the set containing the provided key. If the key is not found, use 'mempty'.@@ -283,19 +355,21 @@ the provided key. -} lookup' :: Ord k => k -> DisjointMap k v -> Maybe v-lookup' !x (DisjointMap p r) =- do x' <- M.lookup x p- if x == x'- then case M.lookup x r of- Nothing -> Nothing- Just (Ranked _ _ v) -> Just v- else find' x'- where find' y = let y' = p M.! y- in if y == y'- then case M.lookup y r of- Nothing -> Nothing- Just (Ranked _ _ v) -> Just v- else find' y'+lookup' !x (DisjointMap p r) = do+ x' <- M.lookup x p+ if x == x'+ then case M.lookup x r of+ Nothing -> Nothing+ Just (Ranked _ _ v) -> Just v+ else find' x'+ where+ find' y =+ let y' = p M.! y+ in if y == y'+ then case M.lookup y r of+ Nothing -> Nothing+ Just (Ranked _ _ v) -> Just v+ else find' y' -- TODO: make this smarter about recreating the parents Map. -- Currently, it will recreate it more often than needed.@@ -305,9 +379,10 @@ helper !x set@(DisjointMap p r) | x == rep = set | otherwise = helper x' set'- where x' = p M.! x- set' = let p' = M.insert x rep p- in p' `seq` DisjointMap p' r+ where+ x' = p M.! x+ set' = let !p' = M.insert x rep p+ in DisjointMap p' r {- $tutorial
test/Spec.hs view
@@ -2,39 +2,91 @@ {-# LANGUAGE ScopedTypeVariables #-} {-# LANGUAGE GeneralizedNewtypeDeriving #-} -import Test.QuickCheck-import Data.Word-import Data.Monoid-import Data.DisjointSet (DisjointSet)+import Data.Aeson (ToJSON,FromJSON)+import Data.Bifunctor (first) import Data.DisjointMap (DisjointMap)-import Data.Set (Set)+import Data.DisjointSet (DisjointSet)+import Data.Enum.Types (C,E,G,H)+import Test.QuickCheck.Instances.Enum () import Data.Foldable (toList)-import Test.QuickCheck.Classes (jsonProps)+import Data.Maybe (mapMaybe)+import Data.Monoid import Data.Proxy (Proxy(..))-import Data.Aeson (ToJSON,FromJSON)+import Data.Set (Set)+import Data.Word+import Test.QuickCheck+import Test.QuickCheck.Classes (jsonLaws,Laws(..))+import Test.Tasty (TestTree,defaultMain,testGroup)++import qualified Data.DisjointMap as DM+import qualified Data.DisjointSet as DS import qualified Data.Foldable as F import qualified Data.Set as S-import qualified Data.DisjointSet as DS-import qualified Data.DisjointMap as DM import qualified GHC.OldList as L+import qualified Test.QuickCheck.Classes as QCC+import qualified Test.Tasty.QuickCheck as TQC main :: IO ()-main = do- putStrLn "\nBeginning QuickCheck Tests"- quickCheck propUnionAll- quickCheck propUnionAppend- quickCheck propSingletons- quickCheck propEquivalances- quickCheck propMapUnionAppend- putStrLn "* Disjoint Set JSON"- F.forM_ (jsonProps (Proxy :: Proxy (DisjointSet Word8))) $ \(name,p) -> do- putStrLn name- quickCheck p- putStrLn "* Disjoint Map JSON"- F.forM_ (jsonProps (Proxy :: Proxy (DisjointMap Word8 WrapWord8))) $ \(name,p) -> do- putStrLn name- quickCheck p+main = defaultMain tests +tests :: TestTree+tests = testGroup "Data"+ [ testGroup "DisjointSet"+ [ testGroup "union"+ [ TQC.testProperty "all" propUnionAll+ , TQC.testProperty "append" propUnionAll+ ]+ , TQC.testProperty "singletons" propSingletons+ , TQC.testProperty "equivalences" propEquivalances+ , lawsToTest (QCC.jsonLaws (Proxy :: Proxy (DisjointSet Word8)))+ , lawsToTest (QCC.jsonLaws (Proxy :: Proxy (DisjointSet Word8)))+ , lawsToTest (QCC.monoidLaws (Proxy :: Proxy (DisjointSet Integer)))+ , lawsToTest (QCC.commutativeMonoidLaws (Proxy :: Proxy (DisjointSet Integer)))+ ]+ , testGroup "DisjointMap"+ [ testGroup "union"+ [ TQC.testProperty "append" propMapUnionAppend+ , TQC.testProperty "order" propMapUnionOrder+ , TQC.testProperty "extra" propMapInsertUnionOrder+ ]+ , TQC.testProperty "insert" propMapInsertOrder+ , lawsToTest (QCC.jsonLaws (Proxy :: Proxy (DisjointMap Word8 WrapWord8)))+ , lawsToTest (QCC.monoidLaws (Proxy :: Proxy (DisjointMap Word8 G)))+ , lawsToTest (QCC.commutativeMonoidLaws (Proxy :: Proxy (DisjointMap Word8 G)))+ ]+ ]++lawsToTest :: QCC.Laws -> TestTree+lawsToTest (QCC.Laws name pairs) = testGroup name (map (uncurry TQC.testProperty) pairs)++propMapUnionOrder :: C -> [Integer] -> C -> [Integer] -> Property+propMapUnionOrder x xs y ys =+ (x /= y)+ ==>+ DM.lookup x (DM.union x y (DM.singleton x xs <> DM.singleton y ys))+ ===+ (xs ++ ys)++propMapInsertOrder :: C -> C -> [Integer] -> [Integer] -> [Integer] -> Property+propMapInsertOrder k j xs ys zs =+ (k /= j)+ ==> + DM.lookup k (DM.insert k xs $ DM.insert j ys $ DM.insert k zs $ mempty)+ ===+ (xs ++ zs)++propMapInsertUnionOrder :: E -> E -> E -> [Integer] -> [Integer] -> [Integer] -> Property+propMapInsertUnionOrder a b c xs ys zs =+ (a /= b)+ ==> + (b /= c)+ ==> + (c /= a)+ ==> + DM.lookup a (DM.union a c (DM.insert a xs $ DM.insert b ys $ DM.insert c zs $ mempty))+ ===+ (xs ++ zs)+ propUnionAll :: [Word] -> Bool propUnionAll xs = let pairs = zip xs (drop 1 xs)@@ -52,13 +104,13 @@ r2 = unionPairs xs1 <> unionPairs xs2 in r1 == r2 -propMapUnionAppend :: [(Word,Word)] -> [(Word,Sum Word)] -> Bool+propMapUnionAppend :: [(Word8,Word8)] -> [(Word8,G)] -> Property propMapUnionAppend xs ys = let r1 = unionMapPairs xs <> mapFromPairs ys (xs1,xs2) = splitList xs (ys1,ys2) = splitList ys r2 = unionMapPairs xs1 <> mapFromPairs ys1 <> unionMapPairs xs2 <> mapFromPairs ys2- in r1 == r2+ in r1 === r2 propSingletons :: [Set Word] -> Bool propSingletons xs = foldMap unionFoldable xs == foldMap DS.singletons xs@@ -111,16 +163,28 @@ instance (Arbitrary k, Ord k, Monoid v, Arbitrary v) => Arbitrary (DisjointMap k v) where arbitrary = do- xs <- arbitrary- ys <- arbitrary+ SmallList xs <- arbitrary+ SmallList ys <- arbitrary let s1 = foldMap (\(k,v) -> DM.singleton k v) (xs :: [(k,v)]) s2 = foldMap (\(k1,k2) -> DM.union k1 k2 DM.empty) (ys :: [(k,k)]) return (s1 <> s2)+ shrink = mapMaybe DM.fromSets . shrink . DM.toSets newtype WrapWord8 = WrapWord8 Word8 deriving (FromJSON,ToJSON,Show,Eq,Arbitrary,Ord) +instance Semigroup WrapWord8 where+ WrapWord8 a <> WrapWord8 b = WrapWord8 (a + b)+ instance Monoid WrapWord8 where mempty = WrapWord8 0 mappend (WrapWord8 a) (WrapWord8 b) = WrapWord8 (a + b) +newtype SmallList a = SmallList { getSmallList :: [a] }++instance Arbitrary a => Arbitrary (SmallList a) where+ arbitrary = do+ n <- choose (0,20)+ xs <- vector n+ return (SmallList xs)+ shrink = map SmallList . shrink . getSmallList