Haskell transitive graph closure
The following program has as its goal the transitive closure of a relation (as a set of ordered pairs - a graph) and a test about the belonging of an ordered pair to this relation.
I tried to make the program efficient by using Data.Set instead of lists and eliminating redundancy in generating the missing pair.
I'd like to know:
- how to use QuickCheck to check if it is correct;
- how to calculate the effectiveness of the program, if possible, or how it compares to similar solutions to the problem (eg, Transient Closure from a list ).
Any criticism and suggestion would be appreciated.
import Data.Set as S
import Data.Foldable as F (foldMap)
data TruthValue = F | U | T deriving (Show,Eq)
isMemberOfTransitiveGraph :: Ord t => (t, t) -> Set (t, t) -> TruthValue
(x,y) `isMemberOfTransitiveGraph` gr
| S.member (x,y) closure = T -- as suggested by user5402
| S.member (y,x) closure = F -- as suggested by user5402
| otherwise = U
where
closure = transitiveClusureOfGraph gr -- as suggested by user5402
transitiveClusureOfGraph :: Ord a => Set (a, a) -> Set (a, a)
transitiveClusureOfGraph gr = F.foldMap (transitiveClosureOfArgument gr) domain
where
domain = S.map fst gr
transitiveClosureOfArgument :: Ord a => Set (a, a) -> a -> Set (a, a)
transitiveClosureOfArgument gr x = S.map ((,) x) $ recursiveImages gr (S.singleton x)
recursiveImages :: Ord a => Set (a, a) -> Set a -> Set a
recursiveImages gr imgs = f gr imgs S.empty
where
f :: Ord a => Set (a, a) -> Set a -> Set a -> Set a
f gr imgs acc
| S.null imgs = acc
| otherwise = f gr (newImgs S.\\ acc) (S.union newImgs acc)
where
newImgs = F.foldMap (imaginsOf gr) imgs
imaginsOf :: (Ord b, Eq a) => Set (a, b) -> a -> Set b
imaginsOf gr arg = S.foldr (\(a,b) acc -> if a == arg then S.insert b acc else acc) S.empty gr
**
EXAMPLE 1
**
someLessThan = S.fromList [("1","2"),("1","4"),("3","4"),("2","8"),("3","5"),("4","7"),("4","8"),("3","9")]
> transitiveClusureOfGraph someLessThan
> fromList [("1","2"),("1","4"),("1","7"),("1","8"),("2","8"),("3","4"),("3","5"),("3","7"),("3","8"),("3","9"),("4","7"),("4","8")]
a `isLessThan` b = (a,b) `isMemberOfTransitiveGraph` someLessThan
> "1" `isLessThan` "8"
> T
> "8" `isLessThan` "1"
> F
> "1" `isLessThan` "9"
> U
> "9" `isLessThan` "1"
> U
**
EXAMPLE 2
**
someTallerThan = S.fromList [("Alexandre","Andrea"),("Andrea","John"),("George","Frank"),("George","Lucy"),("John","Liza"),("Julia","Lucy"),("Liza","Bob"),("Liza","Frank")]
> transitiveClusureOfGraph someTallerThan
> fromList [("Alexandre","Andrea"),("Alexandre","Bob"),("Alexandre","Frank"),("Alexandre","John"),("Alexandre","Liza"),("Andrea","Bob"),("Andrea","Frank"),("Andrea","John"),("Andrea","Liza"),("George","Frank"),("George","Lucy"),("John","Bob"),("John","Frank"),("John","Liza"),("Julia","Lucy"),("Liza","Bob"),("Liza","Frank")]
a `isTallerThan` b = (a,b) `isMemberOfTransitiveGraph` someTallerThan
> "Alexandre" `isTallerThan` "Frank"
> T
> "Frank" `isTallerThan` "Alexandre"
> F
> "Alexandre" `isTallerThan` "George"
> U
> "George" `isTallerThan` "Alexandre"
> U
**
EXAMPLE 3
**
incomeIsLessOrEqualThan = S.fromList [("Bob","Liza"),("Liza","Tom"),("Tom","Bob"),("Tom","Mary"), ("Tom","Tom")]
> S.filter (\(a,b) -> a /= b) $ transitiveClusureOfGraph incomeIsLessOrEqualThan
> fromList [("Bob","Liza"),("Bob","Mary"),("Bob","Tom"),("Liza","Bob"),("Liza","Mary"),("Liza","Tom"),("Tom","Bob"),("Tom","Liza"),("Tom","Mary")]
source to share
Some comments:
-
Some ideas for Quickcheck tests:
- Create a random connected graph and make sure each pair of points is in transitive closure.
- Make sure that for any random graph the transitive closure of the transitive closure is the same as the transitive closure only once.
- Make sure your code returns the same response as the other implementation (e.g. from the fgl library .)
However, when I look at the fgl library I see that they are just using a fixed graph to test their path request functions. Then they know exactly what the answers should be for all tests.
Another idea is to solve the ACM (programming) problem, which involves finding the transitive closure of the graph and using your code in that solution. Both Timus and codeforces accept Haskell programs.
- In
isMemberOfTransitiveGraph
you have a general subexpressiontransitiveClusureOfGraph gr
. Now the GHC can (and should) detect this and decompose it so that it doesn't get evaluated twice, but it doesn't always do it. Moreover, being an interpreter, ghci will not perform generic sub-expression exclusion. So, considering whichtransitiveClusureOfGraph
is an expensive operation, you should write this function like
isMemberOfTransitiveGraph (x,y) gr
| S.member (x,y) closure = T
| S.member (y,x) closure = F
| otherwise = U
where
closure = transitiveClusureOfGraph gr in
-
Also, calculating the transitive closure for an entire graph is an expensive way to determine if a particular pair is in a closure. The best way to implement
isMemberOfTransitiveClosure
is simply search for the beginning of depth in one member of the pair until you a) find another element, or b) fill in the connected component without finding another element. Otherwise, you are doing a lot of work on other related components that are irrelevant to the question you are trying to answer. -
If you are really concerned about efficiency, restrict the node type
Int
and useData.IntSet
or evenData.BitSet
for nodesets.
source to share