Why doesn't GHC provide (isWHNF :: a -> IO Bool)?

I’m quite comfortable with laziness as is in the language, there’s just one thing I find missing: an ability to guarantee that any given value is correctly evaluated.

A good example of a bad way to do this is deepseq: rnf always traverses the entire data structure and gives no feedback on whether it evaluated anything. In an application that correctly evaluates data this function is a complete waste of time.

Moving this functionality to a debug build can be done with an assert, so that’s easy.

The question that remains then is how to check whether a data structure is evaluated. Per the nothunks library, the answer (here) seems to be using GHC.Exts.Heap.getBoxedClosureData to get a copy of an internal representation and analyzing that. This looks ad-hoc and I imagine is also relatively slow with all the repackaging.

So why isn’t there isWHNF :: a -> IO Bool function in GHC internals that copies what seq does without forcing the thunk (similar relationship to one between tryReadMVar and readMVar)?

You can kind of define it yourself:

{- cabal:
build-depends: base, ghc-heap
-}
import GHC.Exts.Heap

isWHNF :: a -> IO Bool
isWHNF x = do
  closure <- getBoxedClosureData $ asBox x
  pure $! case closure of
    ThunkClosure{} -> False
    SelectorClosure{} -> False
    APClosure{} -> False
    BlackholeClosure{} -> False
    _ -> True

Edit: Ah that’s what nothunks does and you already mentioned it.

I don’t think there’s a reason why it was not included, except for “nobody did it yet”. That’s certainly a good addition to ghc-internal. :slight_smile:

Exactly. It’s a very reasonable thing to have.

What does your suggestion resolve that making invalid laziness unrepresentable doesn’t?

In an ideal world levity would be represented on the type level, and the question of “has this piece of data been evaluated before” would hopefully lose all meaning. Until that point, some of the levity information is necessarily lost between definitions, necessitating the existence of a function like isWHNF to recover it.

In regards to “making invalid states unrepresentable”, I consider the general approach of putting bang patterns on polymorphic values and calling the result “strict” a plague on the ecosystem. It is far easier for me to assume I have to evaluate every polymorphic value I care about myself, which I’ll do properly and without bloating the ecosystem with convenience functions/types.

Aside from that, the only thing that remains are spine-lazy/spine-strict data structures, and as far as I’m aware I’m the only person who cares about that distinction (see radix trees).

Really? Wouldn’t it still be a meaningful question for lifted types?

How does that help in practice? I guess when it comes back False you evaluate it? And if True then evaluating it would be a no-op. In which case why not just evaluate it anyway?

I don’t follow. Can you give an example?

No, this function wouldn’t evaluate anything, it would return False if the in-memory representation of a value is a thunk.

Sorry, the “d” was a typo. I mean what do you do when the result is False? I guess you then proceed to evaluate it. If not, what do you do with that information?

I would use it in a debug build of my application via assert to guarantee that specific long-lived values are properly evaluated, logging/crashing when the assertion is broken. The release build then gets all the guarantees for free.

A particular example would be that of a videogame (or any GUI application) where the entire application state is represented with a large sprawling data structure. A single consistent check for proper evaluation would apply to the entire data structure after every action, regardless of what parts of it change. No extra types required.

OK, that makes sense.

This bit I still don’t follow. nothunks is supposed to descend into data structures, but isWHNF wouldn’t. Would you only use it at the top level of a data structure, or would you descend into data structures depending on the value you got back from isWHNF? If the latter, how would it differ from nothunks?

nothunks is just a yet-another-typeclass-library that composes isWHNF. The primary concern, as I initially outlined, is that isWHNF is not directly defined anywhere, so currently one has to settle for an ersatz from ghc-heap.

Right, but I’m not sure if you want isWHNF because it does something different to nothunks (doesn’t descend into data structures) or because it’s a potential primitive that would allow you to write nothunks more efficiently.