RE: Depreciation of elm-lang/lazy library

Elm 0.18 because that’s where I know I can use lazy…

This builds a cycle:

type LazyList a
    = Cons a (Lazy.Lazy (LazyList a))
    | Nil

tail : LazyList a -> Maybe (LazyList a)
tail lazyList =
    case lazyList of
        Cons _ lazyTail ->
            Just <| Lazy.force lazyTail
        Nil ->
            Nothing

makeLazyCycle : a -> LazyList a
makeLazyCycle a =
    let
        lazyTail : Lazy.Lazy (LazyList a)
        lazyTail =
            Lazy.lazy (\_ -> head)

        head =
            Cons a lazyTail
    in
        — The force call is unnecessary but resolves the lazy thunk
        Lazy.force lazyTail

This does not suffer from infinite recursion problems because calling the lazy wrapped function just returns the value of head. It doesn’t crash or create corrupt data structures because at the point when we do invoke the lazy wrapped function, head has been defined and the construction of that function only needed to capture a reference to the variable holding head not the value of head.

But imagine an implementation of Lazy that had no laziness and instead simply called its argument immediately. From a time standpoint this would look different but from a referentially transparent evaluation standpoint it would be the same. Except now, there is no safe order in which to define head and lazyTail. If we define lazyTail first, then when we do so we call the “lazy” wrapped function which gives us the value of head which is presently uninitialized and we end up with lazyTail containing undefined. If we define head first, then lazyTail starts out containing undefined and that’s what we put into the Cons value. Either way, we haven’t crashed yet nor have we gone into infinite recursion, but we just built invalid Elm data structures which will lead to a crash.

What I’m arguing is that in the interest of preventing runtime exceptions, Elm could/should disallow the above construction (either in a let or at module scope where it is also unsafe for the exact same reasons) because the strongly connected component containing the non-function lazyTail in the reference graph also contains the non-function head. Both values need to be computed and there is no way to do so safely. The fact that the reference from lazyTail to head is hidden behind a function just means that that anonymous function is part of the same strongly connected component. If a strongly-connected component in the reference graph requires calls to arbitrary functions to construct any of the values, then it cannot be constructed in a manner that is guaranteed from from runtime exceptions because potentially to construct any of the values all of the other values have to already have been constructed. And as a side-effect of imposing that rule, one rules out cyclic data (or at least restricts it to mutually recursive functions).

Mark