Working on exercises from Okasaki’s Purely Functional Data Structures involving lazy evaluation and wondered if it would be possible to introspect on the structures in memory and see what’s actually going on at runtime.

Note: Scala’s lazy Stream does this, for the cells but not their contents:

scala> val fibs: Stream[Int] = 0 #:: 1 #:: fibs.zip(fibs.tail).map { case (a, b) => a + b }
fibs: Stream[Int] = Stream(0, ?)

scala> fibs(6)
res6: Int = 8

scala> fibs
res7: Stream[Int] = Stream(0, 1, 1, 2, 3, 5, 8, ?)

I found this evaluated function on Stack Overflow (with a whole list of caveats), and slapped together a simple hack. It works for me with GHC 8.6.5 and -O0, but not in ghci or with optimization turned on.

An example test:

  it "shows a list with several cells evaluated" $ do
    let lst = [1..10] :: [Int]
    putStrLn $ tshow $ take 3 lst  -- actually demand the first three values
    showLazyList lst `shouldBe` "1 : 2 : 3 : ..."

And the code:

import GHC.HeapView
import System.IO.Unsafe (unsafePerformIO)

-- [stackoverflow.com/a/2870168...](https://stackoverflow.com/a/28701687/101287)
evaluated :: a -> IO Bool
evaluated = go . asBox
  where
    go box = do
      c <- getBoxedClosureData box
      case c of
        ThunkClosure     {} -> return False
        SelectorClosure  {} -> return False
        APClosure        {} -> return False
        APStackClosure   {} -> return False
        IndClosure       {indirectee = b'} -> go b'
        BlackholeClosure {indirectee = b'} -> go b'
        _ -> return True

showLazyList_ :: (Show a) => String -> String -> [a] -> IO String
showLazyList_ elemThunkStr tailThunkStr lst = do
  evaluated lst >>= \ case
    True -> case lst of
      h : t -> do
        hStr <- evaluated h >>= \ case
                  True -> pure (show h)
                  False -> pure elemThunkStr
        tStr <- showLazyList_ elemThunkStr tailThunkStr t
        pure $ hStr <> " : " <> tStr
      [] -> pure "[]"
    False -> pure tailThunkStr

-- |Show the contents of a list, only so far as have already been evaluated. Handles unevaluated
-- elements and cells. Uses unsafePerformIO shamelessly.
showLazyList :: (Show a) => [a] -> String
showLazyList = unsafePerformIO . showLazyList_ "?" "..."