packages feed

disjoint-containers 0.1.0 → 0.2.0

raw patch · 4 files changed

+299/−39 lines, 4 filesdep +doctestdep ~basePVP ok

version bump matches the API change (PVP)

Dependencies added: doctest

Dependency ranges changed: base

API changes (from Hackage documentation)

+ Data.DisjointMap: foldlWithKeys' :: (a -> Set k -> v -> a) -> a -> DisjointMap k v -> a
+ Data.DisjointMap: instance Data.Foldable.Foldable (Data.DisjointMap.DisjointMap k)
+ Data.DisjointMap: instance Data.Foldable.Foldable (Data.DisjointMap.Ranked k)
+ Data.DisjointMap: instance Data.Traversable.Traversable (Data.DisjointMap.DisjointMap k)
+ Data.DisjointMap: instance Data.Traversable.Traversable (Data.DisjointMap.Ranked k)
+ Data.DisjointMap: instance GHC.Base.Functor (Data.DisjointMap.DisjointMap k)
+ Data.DisjointMap: instance GHC.Base.Functor (Data.DisjointMap.Ranked k)
+ Data.DisjointMap: lookup' :: Ord k => k -> DisjointMap k v -> Maybe v
+ Data.DisjointMap: pretty :: (Show k, Show v) => DisjointMap k v -> String
+ Data.DisjointMap: prettyList :: (Show k, Show v) => DisjointMap k v -> [String]
+ Data.DisjointSet: doubleton :: Ord a => a -> a -> DisjointSet a
+ Data.DisjointSet: equivalent :: Ord a => a -> a -> DisjointSet a -> Bool
+ Data.DisjointSet: pretty :: (Ord a, Show a) => DisjointSet a -> String
+ Data.DisjointSet: sets :: DisjointSet a -> Int
+ Data.DisjointSet: values :: DisjointSet a -> Int
- Data.DisjointMap: lookup :: Ord k => k -> DisjointMap k v -> Maybe v
+ Data.DisjointMap: lookup :: (Ord k, Monoid v) => k -> DisjointMap k v -> v
- Data.DisjointMap: toLists :: Ord k => DisjointMap k v -> [([k], v)]
+ Data.DisjointMap: toLists :: DisjointMap k v -> [([k], v)]

Files

disjoint-containers.cabal view
@@ -1,5 +1,5 @@ name: disjoint-containers-version: 0.1.0+version: 0.2.0 synopsis: Disjoint containers description: Disjoint containers homepage: https://github.com/andrewthad/disjoint-containers#readme@@ -34,6 +34,18 @@     , containers     , QuickCheck   default-language: Haskell2010++test-suite doctest+  type: exitcode-stdio-1.0+  hs-source-dirs: test+  main-is: Doctest.hs+  build-depends:+      base+    , disjoint-containers+    , doctest >= 0.10+    , QuickCheck+  default-language:    Haskell2010+  source-repository head   type: git
src/Data/DisjointMap.hs view
@@ -1,18 +1,48 @@ {-# LANGUAGE BangPatterns #-}+{-# LANGUAGE DeriveFunctor #-}+{-# LANGUAGE DeriveFoldable #-}+{-# LANGUAGE DeriveTraversable #-}  {-# OPTIONS_GHC -Wall #-} +{-|+Maps with disjoint sets as the key. The type in this module can be+understood as:++> DisjointMap k v ≡ Map (DisjointSet k) v++However, actually using a 'Map' with @DisjointSet@ as the key+would offer terrible performance. This is because the 'Ord'+instance for @DisjointSet@ cannot be made to perform well.+Internally, @DisjointMap@ is implemented like a disjoint set+but the data structure that maps representatives to their rank also holds the value+associated with that representative element. Additionally, it holds the set+of all keys that in the same equivalence class as the representative.+This makes it possible to implementat functions like @foldlWithKeys\'@+efficiently.++-}+ module Data.DisjointMap   ( DisjointMap+    -- * Construction   , empty   , singleton   , singletons   , insert   , union+    -- * Query   , lookup+  , lookup'   , representative   , representative'+    -- * Conversion   , toLists+  , pretty+  , prettyList+  , foldlWithKeys'+    -- * Tutorial+    -- $tutorial   ) where  import Prelude hiding (lookup)@@ -24,15 +54,23 @@ import Data.Map (Map) import Data.Set (Set) import Data.Bifunctor (first)+import Data.Foldable (Foldable)+import Data.Maybe (fromMaybe) import qualified Data.Map.Strict as M-import qualified Data.Map.Merge.Strict as MM import qualified Data.Set as S+import qualified GHC.OldList as L +-- | A map having disjoints sets of @k@ as keys and+--   @v@ as values. data DisjointMap k v = DisjointMap   !(Map k k) -- parents and values-  !(Map k (Ranked v)) -- ranks+  !(Map k (Ranked k v)) -- ranks+  deriving (Functor,Foldable,Traversable) -data Ranked b = Ranked {-# UNPACK #-} !Int !b+-- the name ranked is no longer totally appropriate since+-- a set of keys has been added in here as well.+data Ranked k v = Ranked {-# UNPACK #-} !Int !(Set k) !v+  deriving (Functor,Foldable,Traversable)  instance (Ord k, Monoid v) => Monoid (DisjointMap k v) where   mappend = append@@ -52,25 +90,30 @@ showDisjointSet :: (Show k, Ord k, Show v) => DisjointMap k v -> String showDisjointSet = show . toLists -toLists :: Ord k => DisjointMap k v -> [([k],v)]+toLists :: DisjointMap k v -> [([k],v)] toLists = (fmap.first) S.toList . toSets -toSets :: Ord k => DisjointMap k v -> [(Set k,v)]-toSets dm@(DisjointMap _ r) = M.elems $ MM.merge MM.dropMissing MM.dropMissing (MM.zipWithMatched $ \_ ks (Ranked _ v) -> (ks,v)) (flatten dm) r+toSets :: DisjointMap k v -> [(Set k,v)]+toSets (DisjointMap _ r) = M.foldr+  (\(Ranked _ s v) xs -> (s,v) : xs) [] r --- in the result of this, the key in the--- map keeps everything separate.-flatten :: Ord k => DisjointMap k v -> Map k (Set k)-flatten ds@(DisjointMap p _) = S.foldl'-  ( \m a -> case find a ds of-    Nothing -> error "DisjointMap flatten: invariant violated. missing key."-    Just b -> M.insertWith S.union b (S.singleton a) m-  ) M.empty (M.keysSet p)+pretty :: (Show k, Show v) => DisjointMap k v -> String+pretty dm = "{" ++ L.intercalate ", " (prettyList dm) ++ "}" +prettyList :: (Show k, Show v) => DisjointMap k v -> [String]+prettyList dm = L.map (\(ks,v) -> "{" ++ commafied ks ++ "} -> " ++ show v) (toSets dm)++commafied :: Show k => Set k -> String+commafied = join . L.intersperse "," . map show . S.toList++foldlWithKeys' :: (a -> Set k -> v -> a) -> a -> DisjointMap k v -> a+foldlWithKeys' f a0 (DisjointMap _ r) =+  M.foldl' (\a (Ranked _ ks v) -> f a ks v) a0 r+ {-| Create an equivalence relation between x and y. If either x or y are not already is the disjoint set, they are first created-as singletons.+as singletons with a value that is 'mempty'. -} union :: (Ord k, Monoid v) => k -> k -> DisjointMap k v -> DisjointMap k v union !x !y set = flip execState set $ runMaybeT $ do@@ -78,50 +121,59 @@   repy <- lift $ state $ lookupCompressAdd y   guard $ repx /= repy   DisjointMap p r <- lift get-  let Ranked rankx valx = r M.! repx-  let Ranked ranky valy = r M.! repy+  let Ranked rankx keysx valx = r M.! repx+  let Ranked ranky keysy valy = r M.! repy   let val = mappend valx valy+      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 val) r+              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 val) r+              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) val) r+              r' = M.delete repx $! M.insert repy (Ranked (ranky + 1) keys val) r           in  DisjointMap p' r'  {-|-Find the set representative for this input.+Find the set representative for this input. This function+ignores the values in the map. -} representative :: Ord k => k -> DisjointMap k v -> Maybe k representative = find -{-| Insert x into the disjoint set.  If it is already a member,-    then do nothing, otherwise x has no equivalence relations.-    O(logn).+{-| 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.+    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 !x !v set@(DisjointMap p r) =+insert !x = insertInternal x (S.singleton x)++insertInternal :: (Ord k, Monoid 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         Just _ ->-          let (m,DisjointMap p' r') = representative' x set+          let (m,DisjointMap p2 r') = representative' x set            in case m of                 Nothing -> error "DisjointMap insert: invariant violated"-                Just root -> DisjointMap p' (M.adjust (\(Ranked rank vOld) -> Ranked rank (mappend v vOld)) root r')+                Just root -> DisjointMap p2 (M.adjust (\(Ranked rank oldKs vOld) -> Ranked rank (mappend oldKs ks) (mappend v vOld)) root r')         Nothing ->-          let r' = M.insert x (Ranked 0 v) r+          let r' = M.insert x (Ranked 0 ks v) r           in  DisjointMap p' r' -{-| Create a disjoint set with one member. O(1). -}++{-| Create a disjoint map with one key: a singleton set. O(1). -} singleton :: k -> v -> DisjointMap k v singleton !x !v =   let p = M.singleton x x-      r = M.singleton x (Ranked 0 v)+      r = M.singleton x (Ranked 0 (S.singleton x) v)    in DisjointMap p r +{-| The empty map -} empty :: DisjointMap k v empty = DisjointMap M.empty M.empty @@ -130,20 +182,23 @@   then appendParents r2 s1 m2   else appendParents r1 s2 m1 -appendParents :: (Ord k, Monoid v) => Map k (Ranked v) -> DisjointMap k v -> Map k k -> DisjointMap k v+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   then case M.lookup x ranks of     Nothing -> error "DisjointMap appendParents: invariant violated"-    Just (Ranked _ v) -> insert x v ds+    Just (Ranked _ ks v) -> insertInternal x ks v ds   else union x y ds -{-| Create a disjoint set where all members are equal. -}+{-| Create a disjoint map with one key. Everything in the+    'Set' argument is consider part of the same equivalence+    class.+-} singletons :: Eq k => Set k -> v -> DisjointMap k v singletons s v = case S.lookupMin s of   Nothing -> empty   Just x ->     let p = M.fromSet (\_ -> x) s-        r = M.singleton x (Ranked 1 v)+        r = M.singleton x (Ranked 1 s v)     in DisjointMap p r  {-|@@ -171,19 +226,28 @@   where find' y = let y' = p M.! y                   in  if y == y' then y' else find' y' -lookup :: Ord k => k -> DisjointMap k v -> Maybe v-lookup !x (DisjointMap p r) =+{-| Find the value associated with the set containing+    the provided key. If the key is not found, use 'mempty'.+-}+lookup :: (Ord k, Monoid v) => k -> DisjointMap k v -> v+lookup k = fromMaybe mempty . lookup' k++{-| Find the value associated with the set containing+    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+         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+                          Just (Ranked _ _ v) -> Just v                         else find' y'  -- TODO: make this smarter about recreating the parents Map.@@ -198,4 +262,54 @@           set'  = let p' = M.insert x rep p                   in  p' `seq` DisjointMap p' r +{- $tutorial++The disjoint map data structure can be used to model scenarios where+the key of a map is a disjoint set. Let us consider trying to find+the lowest rating of our by town. Due to differing subcultures,+inhabitants do not necessarily agree on a canonical name for each town.+Consequently, the survey allows participants to write in their town+name as they would call it. We begin with a rating data type:++>>> import Data.Function ((&))+>>> data Rating = Lowest | Low | Medium | High | Highest deriving (Eq,Ord,Show)+>>> instance Monoid Rating where mempty = Highest; mappend = min++Notice that the 'Monoid' instance combines ratings by choosing+the lower one. Now, we consider the data from the survey:++>>> let resA = [("Big Lake",High),("Newport Lake",Medium),("Dustboro",Low)]+>>> let resB = [("Sand Town",Medium),("Sand Town",High),("Mount Lucy",High)]+>>> let resC = [("Lucy Hill",Highest),("Dustboro",High),("Dustville",High)]+>>> let m1 = foldMap (uncurry singleton) (resA ++ resB ++ resC)+>>> :t m1+m1 :: DisjointMap [Char] Rating+>>> mapM_ putStrLn (prettyList m1)+{"Big Lake"} -> High+{"Dustboro"} -> Low+{"Dustville"} -> High+{"Lucy Hill"} -> Highest+{"Mount Lucy"} -> High+{"Newport Lake"} -> Medium+{"Sand Town"} -> Medium++Since we haven't defined any equivalence classes for the town names yet,+what we have so far is no different from an ordinary 'Map'. Now we+will introduce some equivalences:++>>> let m2 = m1 & union "Big Lake" "Newport Lake" & union "Sand Town" "Dustboro"+>>> let m3 = m2 & union "Dustboro" "Dustville" & union "Lucy Hill" "Mount Lucy"+>>> mapM_ putStrLn (prettyList m3)+{"Dustboro","Dustville","Sand Town"} -> Low+{"Lucy Hill","Mount Lucy"} -> High+{"Big Lake","Newport Lake"} -> Medium++We can now query the map to find the lowest rating in a given town:++>>> lookup "Dustville" m3+Low+>>> lookup "Lucy Hill" m3+High++-} 
src/Data/DisjointSet.hs view
@@ -2,16 +2,38 @@  {-# OPTIONS_GHC -Wall #-} +{-|+Persistent disjoint-sets. 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.+This library provides the fundamental operations classically+known as @union@, @find@, and @makeSet@. It also offers+novelties like a 'Monoid' instance for disjoint sets+and conversion functions for interoperating with lists.+See the tutorial at the bottom of this page for an example+of how to use this library.+-}+ module Data.DisjointSet   ( DisjointSet+    -- * Construction   , empty   , singleton   , singletons+  , doubleton   , insert   , union+    -- * Query+  , equivalent+  , sets+  , values   , representative   , representative'+    -- * Conversion   , toLists+  , pretty+    -- * Tutorial+    -- $tutorial   ) where  import Prelude hiding (lookup)@@ -23,9 +45,11 @@ import Data.Map (Map) import Data.Set (Set) import Data.Semigroup (Semigroup)+import Data.Maybe (fromMaybe) import qualified Data.Semigroup import qualified Data.Map.Strict as M import qualified Data.Set as S+import qualified Data.List as L  data DisjointSet a = DisjointSet   !(Map a a) -- parents@@ -50,6 +74,17 @@ showDisjointSet :: (Show a, Ord a) => DisjointSet a -> String showDisjointSet = show . toLists +pretty :: (Ord a, Show a) => DisjointSet a -> String+pretty xs = id+  . showChar '{'+  . applyList (L.intersperse (showChar ',') (map (\x -> showChar '{' . applyList (L.intersperse (showChar ',') (map shows x)) . showChar '}') (toLists xs)))+  . showChar '}'+  $ []++applyList :: [(a -> a)] -> a -> a+applyList [] = id+applyList (f : fs) = f . applyList fs+ toLists :: Ord a => DisjointSet a -> [[a]] toLists = map S.toList . toSets @@ -95,6 +130,21 @@ representative :: Ord a => a -> DisjointSet a -> Maybe a representative = find +{-| Decides whether the two values belong to the same set -}+equivalent :: Ord a => a -> a -> DisjointSet a -> Bool+equivalent a b ds = fromMaybe False $ do+  x <- representative a ds+  y <- representative b ds+  Just (x == y)++{-| Count the number of disjoint sets -}+sets :: DisjointSet a -> Int+sets (DisjointSet _ r) = M.size r++{-| Count the total number of values contained by the disjoint sets -}+values :: DisjointSet a -> Int+values (DisjointSet p _) = M.size p+ {-| Insert x into the disjoint set.  If it is already a member,     then do nothing, otherwise x has no equivalence relations.     O(logn).@@ -115,6 +165,12 @@       r = M.singleton x 0    in DisjointSet p r +{-| Create a disjoint set with a single set containing two members -}+doubleton :: Ord a => a -> a -> DisjointSet a+doubleton a b = union a b empty+-- doubleton could be more efficient++{-| The empty set of disjoint sets. -} empty :: DisjointSet a empty = DisjointSet M.empty M.empty @@ -162,6 +218,8 @@   where find' y = let y' = p M.! y                   in  if y == y' then y' else find' y' ++ -- TODO: make this smarter about recreating the parents Map. -- Currently, it will recreate it more often than needed. compress :: Ord a => a -> a -> DisjointSet a -> DisjointSet a@@ -172,4 +230,73 @@               where x'    = p M.! x                     set'  = let p' = M.insert x rep p                             in  p' `seq` DisjointSet p' r++{- $tutorial++The disjoint set data structure represents sets that are+disjoint. Each set in the data structure can be interpreted+as an equivalance class. For example, let us consider a scenario+in which we are investigating spies who each use one or more aliases. There are three+actions we may repeated take:++    1. we learn an alias is in use by someone (make set)+    2. we learn two aliases refer to the same individual (union)+    3. we check our notes to figure out if two aliases refer to the same individual (find)++We initially learn of the existence of several aliases:++>>> import Data.Function ((&))+>>> import Data.Monoid ((<>))+>>> import Data.Foldable (fold,foldMap)+>>> let s0 = empty+>>> let s1 = s0 & insert "Scar" & insert "Carene" & insert "Barth" & insert "Coral"+>>> let s2 = s1 & insert "Boris" & insert "Esma" & insert "Mayra"+>>> putStr (pretty s2)+{{"Barth"},{"Boris"},{"Carene"},{"Coral"},{"Esma"},{"Mayra"},{"Scar"}}++Note that the 'Monoid' instance gives us a way to construct this more succintly:++>>> s2 == foldMap singleton ["Barth","Boris","Carene","Coral","Esma","Mayra","Scar"]+True++After some preliminary research, we learn that Barth and Esma are the same person. We+also learn that Carene and Mayra are the same:++>>> let s3 = s2 & union "Barth" "Esma" & union "Carene" "Mayra"+>>> putStr (pretty s3)+{{"Boris"},{"Coral"},{"Barth","Esma"},{"Carene","Mayra"},{"Scar"}}++Another informant comes forward who tells us they have worked for someone+that went by the names Mayra and Esma. Going through old letters, we learn+that Boris is a pen name used by Scar:++>>> let s4 = s3 & union "Mayra" "Esma" & union "Boris" "Scar"+>>> putStr (pretty s4)+{{"Coral"},{"Barth","Carene","Esma","Mayra"},{"Boris","Scar"}}++At this point, Detective Laura from another department drops by with+questions about a case she is working on. She asks if Boris the same+person as Barth and if Carene is the same person as Esma. We answer:++>>> equivalent "Boris" "Barth" s4+False+>>> equivalent "Carene" "Esma" s4+True++The correct way to interpret this is that @False@ means something more+along the lines of unknown, but we definitely know that Carene is Esma.+Finally, before the detective leaves, she gives us some of her case+notes to synthesize with our information. Notice that there are+some aliases she encountered that we did not and vice versa:++>>> let laura = union "Scar" "Coral" $ union "Esma" "Henri" $ foldMap singleton ["Carene","Boris","Barth"]+>>> putStr (pretty laura)+{{"Barth"},{"Boris"},{"Carene"},{"Coral","Scar"},{"Esma","Henri"}}+>>> putStr (pretty (laura <> s4))+{{"Barth","Carene","Esma","Henri","Mayra"},{"Boris","Coral","Scar"}}++With Laura's shared findings, we now see that there are really only (at most)+two spies that we are dealing with.++-} 
+ test/Doctest.hs view
@@ -0,0 +1,7 @@+import Test.DocTest++main :: IO ()+main = doctest+  [ "src/Data/DisjointSet.hs"+  , "src/Data/DisjointMap.hs"+  ]