[Haskell-cafe] Detecting Cycles in Datastructures
Paul Hudak
paul.hudak at yale.edu
Fri Nov 18 14:28:24 EST 2005
Greg Woodhouse wrote:
> --- Paul Hudak <paul.hudak at yale.edu> wrote:
>>Tom Hawkins wrote:
>> > In a pure language, is it possible to detect cycles in recursive
>> > data structures? For example, is it possible to determine that
>> > "cyclic" has a loop? ...
>> >
>> > data Expr = Constant Int | Addition Expr Expr
>> >
>> > cyclic :: Expr
>> > cyclic = Addition (Constant 1) cyclic
>> >
>> > Or phased differently, is it possible to make "Expr" an instance of
>> > "Eq" such that cyclic == cyclic is smart enough to avoid a
>> > recursive decent?
>
> I'm a little confused. It's one thing to demonstrate that a (possibly
> infinite) digraph contains a loop, but quite another to show that it
> does not. Carrying around a copy of the subgraph consisting of nodes
> actually visited is workable in a pure language, but there's no
> guarantee of termination.
The question that was originally posted assumed that the graph was
represented using direct recursion in Haskell. In which case you
cannot detect a cycle, as previous replies pointed out. Of course, if
you represented the graph in some more concrete form -- such as an
explicit list of nodes and edges -- then you could detect the cycle
using a standard graph algorithm, at the expense of losing the
elegance of the Haskell recursion. My post was just pointing out that
there is a third possibility, one that retains most of the elegance
and abstractness of Haskell, yet still allows detecting cycles.
>>Perhaps it's obvious, but one way to do this is to make the cycle
>>*implicit* via some kind of a fixed-point operator, which is a trick
>>I used recently in a DSL application.
>
> So, you imagine f to be a function that (non-deterministally) follows
> an edge in a digraph. The idea being that G is (possibly infinite)
> digraph and F is a subgraph. The "function" f would
> non-deterministically select a vertext of F, follow an edge in G (again
> chosen non-deterministically), add the (possibly new) edge to F, and
> return the resulting graph. Then, you are asking if f has a fixpoint in
> this broader context?
I don't understand this... and therefore it's probably not what I
imagine! I'm saying simply that a cyclic graph can be represented as
the fixed point of a function.
>>For example, you could define:
>>
>> > data Expr = Const Int
>> > | Add Expr Expr
>> > | Loop -- not exported
>> > deriving (Eq, Show)
>>
>>The purpose of Loop is to "mark" the point where a cycle exists.
>>Instead of working with values of type Expr, you work with values of
>>type Expr -> Expr, or Fix Expr:
>>
>> > type Fix a = a -> a
>>
>>For example:
>>
>> > fe1,fe2 :: Fix Expr
>> > fe1 e = Add (Const 1) (Const 1) -- non-recursive
>> > fe2 e = Add (Const 1) e -- recursive
>>
>>You can always get the value of an Expr from a Fix Expr whenever you
>>need it, by computing the fixed point:
>>
>> > fix f = x where x = f x
>
> If it can be computed. Maybe I'm off-track here, but it seems to me
> that when you introduce laziness into the equation, you lose any
> guarantee of there even being a fixpoint, much less one that can be
> computed.
Actually it's the other way around -- laziness is what makes this work!
The least fixpoint of fe2 in a strict language, for example, is bottom.
>> > e1,e2 :: Expr
>> > e1 = fix fe1 -- finite
>> > e2 = fix fe2 -- infinite
>>
>>Note that e2 is equivalent to your "cyclic". But now we can also
>>test for equality:
>>
>> > instance Eq (Fix Expr) where
>> > fe1 == fe2 = fe1 Loop == fe2 Loop
>>
>>For example, fe2 == fe2 returns "True", whereas e2 == e2
>>(i.e. your cyclic == cyclic) diverges.
>>
>>Of course, note that, although fe1 == fe2 implies that fix fe1 == fix
>>fe2, the reverse is not true, since there will always be loops of
>>varying degrees of unwinding that are semantically equivalent, but
>>not "syntactically", or structurally, equivalent. Thus this definition
>>of equality is only approximate, but still, it's useful.
>
> I'm lost. Are you saying bottom is bottom?
I suspect from your other post that you haven't seen the standard
trick of encoding infinite data structures as fixpoints. Suppose you
have a lambda calculus term for cons, as well as for the numeral 1.
Then the infinite list of ones is just:
Y (\ones. cons 1 ones)
where Y (aka the "paradoxical combinator" or "fixed point combinator")
is defined as:
\f. (\x. f (x x)) (\x. f (x x))
To see this, try unfolding the above term a few iterations using
normal-order reduction. Note that the term has no recursion in it
whatsoever.
Now, my earlier point above is that:
Y (\ones. cons 1 ones)
unwinds to the same term as:
Y (\ones. cons 1 (cons 1 ones))
yet the two terms (\ones. cons 1 ones) and
(\ones. cons 1 (cons 1 ones)) are not the same (in fact they're not
lambda convertible, either).
-Paul
More information about the Haskell-Cafe
mailing list