Skip to content

Commit 3730642

Browse files
Use containers' scc algorithm to speed up findCycle (#59)
Co-authored-by: Joosep Jääger <[email protected]>
1 parent f270750 commit 3730642

File tree

2 files changed

+22
-24
lines changed

2 files changed

+22
-24
lines changed

src/Constrained/Graph.hs

Lines changed: 16 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -25,14 +25,16 @@ module Constrained.Graph (
2525

2626
import Control.Monad
2727
import Data.Foldable
28-
import Data.List (sortOn, nub)
28+
import Data.List (nub)
2929
import Data.Map (Map)
3030
import Data.Map qualified as Map
3131
import Data.Maybe
3232
import Data.Set (Set)
3333
import Data.Set qualified as Set
3434
import Prettyprinter
3535
import Test.QuickCheck
36+
-- TODO: consider using more of this
37+
import Data.Graph qualified as G
3638

3739
-- | A graph with unlabeled edges for keeping track of dependencies
3840
data Graph node = Graph
@@ -166,21 +168,22 @@ topsort gr@(Graph e _) = go [] e
166168
removeNode n ds = Set.difference ds noDeps <$ guard (not $ n `Set.member` noDeps)
167169
if not $ null noDeps
168170
then go (Set.toList noDeps ++ order) (Map.mapMaybeWithKey removeNode g)
169-
else Left . concat . take 1 . sortOn length . filter (not . null) . map (findCycle gr) $ Map.keys e
171+
else Left $ findCycle gr
170172

171173
-- | Simple DFS cycle finding
172-
findCycle :: Ord node => Graph node -> node -> [node]
173-
findCycle g@(Graph e _) node = concat . take 1 $ filter loopy $ go mempty node
174+
findCycle :: Ord node => Graph node -> [node]
175+
findCycle g@(Graph e _) = mkCycle . concat . take 1 . filter isCyclic . map (map tr) . concatMap cycles . G.scc $ gr
174176
where
175-
loopy [] = False
176-
loopy c@(x:_) = dependsOn (last c) x g
177-
go seen n
178-
| n `Set.member` seen = [[]]
179-
| otherwise = do
180-
n' <- neighbours
181-
(n :) <$> go (Set.insert n seen) n'
182-
where
183-
neighbours = maybe [] Set.toList $ Map.lookup n e
177+
edgeList = [ (n, n, Set.toList es) | (n, es) <- Map.toList e ]
178+
(gr, tr0, _) = G.graphFromEdges edgeList
179+
tr x = let (n, _, _) = tr0 x in n
180+
cycles (G.Node a []) = [[a]]
181+
cycles (G.Node a as) = (a:) <$> concatMap cycles as
182+
isCyclic [] = False
183+
isCyclic [a] = dependsOn a a g
184+
isCyclic _ = True
185+
-- Removes a non-dependent stem from the start of the dependencies
186+
mkCycle ns = let l = last ns in dropWhile (\ n -> not $ dependsOn l n g) ns
184187

185188
-- | Get the dependencies of a node in the graph, `mempty` if the node is not
186189
-- in the graph

test/Constrained/GraphSpec.hs

Lines changed: 6 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -91,25 +91,20 @@ prop_topsort_sound g =
9191

9292
prop_topsort_complete :: Graph Node -> Property
9393
prop_topsort_complete g =
94-
isLeft (topsort g) === any (\ n -> not . null $ findCycle g n) (nodes g)
94+
isLeft (topsort g) === not (null $ findCycle g)
9595

9696
prop_find_cycle_sound :: Property
9797
prop_find_cycle_sound =
9898
forAllShrink (mkGraph @Node <$> arbitrary) shrink $ \ g ->
99-
and [ all (\(x, y) -> dependsOn x y g) (zip c (drop 1 c))
100-
| n <- Set.toList $ nodes g
101-
, let c = findCycle g n
102-
]
99+
let c = findCycle g
100+
in counterexample (show c) $ all (\(x, y) -> dependsOn x y g) (zip c (drop 1 $ cycle c))
103101

104102
prop_find_cycle_loops :: Property
105103
prop_find_cycle_loops =
106104
forAllShrink (mkGraph @Node <$> arbitrary) shrink $ \ g ->
107-
conjoin
108-
[ case findCycle g n of
109-
[] -> property True
110-
c@(x:_) -> cover 40 True "found cycle" $ counterexample (show c) $ dependsOn (last c) x g
111-
| n <- Set.toList $ nodes g
112-
]
105+
case findCycle g of
106+
[] -> property True
107+
c@(x:_) -> cover 40 True "found cycle" $ counterexample (show c) $ dependsOn (last c) x g
113108

114109
return []
115110

0 commit comments

Comments
 (0)