The blog post introduces how we can force full evaluation using DeepSeq in Haskell.

Recap
In the last article, we discussed how the seq
function forces evaluation of the first argument until
it is in weak head normal form (WHNF), which is different from normal form. But how can we force
evaluation until an expression is in normal form or fully evaluated? This is where DeepSeq
comes in.
NFData
In Control.DeepSeq
, there is a typeclass NFData
which stands for normal form data. There are
many predefined datatypes that are already defined as valid instances of NFData
within the package.
import Control.DeepSeq
instance NFData Bool
instance NFData Char
instance NFData Maybe
-- etc.
For a datatype to be a valid instance of the NFData
typeclass, you will need to define the rnf
(reduce to normal form) function,
which forces full evaluation of the argument in the data type.
data PeaNum = Succ PeaNum | Zero deriving Show
instance NFData PeaNum where
rnf Zero = ()
rnf (Succ a) = rnf a
In the above example, we defined PeaNum
recursively and made it an instance of NFData
by defining rnf
that
recursively apply rnf
until it reaches Zero
. This can fully evaluates PeaNum
. Let's look at another example.
data Tree a = Node (Tree a) a (Tree a) | Leaf deriving Show
-- You need `a` to be also a valid instance of NFData for
-- fully evaluating Tree a
instance NFData a => NFData (Tree a) where
rnf Leaf = ()
rnf (Node l v r) = rnf l `seq` rnf v `seq` rnf r
The example above uses the seq
function to force the evaluation of the left-subtree before evalating v
and then evalating r
.
deepseq
Once we make a data type a valid instance of NFData
we can use the deepseq
function to convert the data to normal form
by calling rnf
.
five = Succ $ Succ $ Succ $ Succ $ Succ Zero
-- ghci> :sprint five
-- five = _
-- ghci> deepseq five ()
-- ()
-- ghci> :sprint five
-- five = Succ (Succ (Succ (Succ (Succ Zero))))
Instead of the seq
function that only evaluates until it is in WHNF (five = Succ _
), the deepseq
function evaluates fully
until it is in normal form (five = Succ (Succ (Succ (Succ (Succ Zero))))
).
Why DeepSeq?
In some cases, we really need to fully evaluate an expression, such as in the IO action like below.
import System.IO
import Control.DeepSeq
main = do
h <- openFile "f" ReadMode
s <- hGetContents h
s `deepseq` hClose h
return s
In the above IO action, we open a file, get the content in the file, and close the file. Before we close the file,
we need to make sure that we have obtained the content by fully evaluating hGetContents h
so that we are not left with a thunk that
cannot be obtained after closing the file. In such cases, we can use deepseq
.
As we have discussed already, Haskell code should remain lazy and only has strictness where it is unavoidable. Be sure to use it wisely.
Exercises
This is an exercise section where you can test your understanding of the material introduced in the article. I highly recommend solving these questions by yourself after reading the main part of the article. You can click on each question to see its answer.
Resources
- Philipp, Hagenlocher. 2020. Haskell for Imperative Programmers #32 - DeepSeq. YouTube.
- nd. Control.DeepSeq. deepseq-1.4.1.1: Deep evaluation of data structures.