I have just uploaded a new version of ghc-heap-view to Hackage that provides “Evaluation state assertions” in the module GHC.AssertNF
.
Imagine you are writing a web application in Haskell that sports a global number-of-visitors counter in an IORef Int
. For every request, you call modifyIORef (+1)
. Eventually, you notice your very popular web site to hog more and more memory. So you browse to the internal page that shows the counter, and you have to wait for a long time until you eventually see the result (or get a stack overflow). The reason: The applications of (+1)
were not performed until you looked at the number; instead, a long chain of such computation first filled your heap and then your stack.
So you have learned the hard way that you might want to avoid space leaks, and want calculations to be done during the request that caused them, and want the IORef
to always contain fully evaluated data. So you stumble about modifyIORef'
in Data.IORef
and indeed, this fixes your problem.
Later, you notice that you want to count POST
and GET
requests separately. You change the type to IORef (Int, Int)
and call modifyIORef' (first (+1))
or modifyIORef' (second (+1))
. And suddenly, the space leak is back (which you only notice after the next push to the real site, because your local tests never caused enough requests to make it noticeable). So you not only want to fix it, you also want to ensure that it does not break again.
In other words, you want to ensure the policy that values stored in an IORef
are always in normal form. You achieve this with the following alternative to modifyIORef'
:
modifyIORef'Assert :: IORef a -> (a -> a) -> IO () modifyIORef'Assert ref f = do x <- readIORef ref let x' = f x x' `seq` return () assertNF x' writeIORef ref x'
Using this instead of modifyIORef'
will print this warning to standard error output right the first time you call modifyIORef'Assert (first (+1))
:
Parameter not in normal form: 2 thunks found: let x1 = (S# 1,S# 1) in _bh (_thunk x1 (_bco (S# 1)),_sel x1)
(Otherwise, the program runs as usual.) So obviously, you need to use a strict variant of first
(or strict pairs):
first' :: (a -> b) -> (a, c) -> (b, c) first' f (x,y) = let { x' = f x; r = (x', y) } in x' `seq` r `seq` r
With this, the warning goes away. Whenever you now change the type of the IORef
or modify it in a too-lazy-way, you can be sure that you’ll be warned about it, before the space leak itself becomes noticeable.
In the production code, you might want to disable the check. For that, simply put disableAssertNF
somewhere in your main function.
Why is this better than just calling deepseq
in modifyIORef'Assert
? Because this way, the code still creates unwanted thunks that are then evaluated before storing them in the IORef
, whereas with assertNF
you are told about the thunks and can prevent them from being created in the first place. Also, assertNF
does not add a type class constraint.
This is just one example application for assertNF
(and its variants assertNFNamed
, which includes a name in the warning to better spot the cause, and $assertNFHere
, which uses Template Haskell to include the current source code position in the warning), and I hope that there are more. If you happen to make use of it, I’d like to hear your story.
Have something to say? You can post a comment by sending an e-Mail to me at <mail@joachim-breitner.de>, and I will include it here.
I think you forgot to push your private darcs branch to the public repo :-).
Cheers,
Erik