Strictness and Space Leaks
Prerequisites: Laziness
Laziness is usually a win, but when a program keeps building thunks that are only evaluated much later, memory grows for no reason — a space leak. This lesson covers the tools that control evaluation.
Weak head normal form
Forcing a value in Haskell evaluates it only to its outermost constructor — called weak head normal form (WHNF):
main :: IO ()
main = do
let pair = (error "left", error "right") :: (Int, Int)
pair `seq` putStrLn "the pair is in WHNF; its contents were not touched"
let xs = [1, error "boom"] :: [Int]
length xs `seq` putStrLn "length only needs the list's spine"
the pair is in WHNF; its contents were not touched
length only needs the list's spine
seq forced pair to a tuple constructor — without evaluating either
error. To evaluate everything inside a value, use deepseq or
force from the deepseq package.
Bang patterns and strict accumulators
{-# LANGUAGE BangPatterns #-}
mean :: [Double] -> Double
mean = go 0 0
where
go :: Double -> Int -> [Double] -> Double
go !total !count [] = total / fromIntegral count
go !total !count (x : xs) = go (total + x) (count + 1) xs
main :: IO ()
main = print (mean [1 .. 1000000])
500000.5
A ! on a pattern evaluates the argument before the function body runs,
so total and count are plain numbers on every step rather than
growing chains of +. (BangPatterns is part of GHC2021, so the pragma
is optional with modern GHC.)
Strict data fields
import Data.List (foldl')
data Stats = Stats
{ count :: !Int
, total :: !Double
}
deriving (Show)
addSample :: Stats -> Double -> Stats
addSample (Stats c t) x = Stats (c + 1) (t + x)
main :: IO ()
main = print (foldl' addSample (Stats 0 0) [1 .. 100000])
Stats {count = 100000, total = 5.00005e9}
foldl' only evaluates the accumulator to WHNF — the Stats
constructor. The ! on each field makes building a Stats evaluate its
fields too, so no thunks pile up inside. A good default: make record
fields strict unless you specifically need laziness (the StrictData
extension does this for a whole module).
deepseq
import Control.DeepSeq (force, deepseq)
import Control.Exception (evaluate)
main :: IO ()
main = do
let squares = map (^ 2) [1 .. 5 :: Int]
squares `deepseq` putStrLn "every element has been computed"
table <- evaluate (force (zip [1 :: Int ..] "abc"))
print table
every element has been computed
[(1,'a'),(2,'b'),(3,'c')]
evaluate (force x) is the standard way to make sure a value is fully
computed at a particular point — for example, before timing it, or
before handing it to another thread.
Common sources of leaks
foldl, lazyState, andWriteraccumulating over long loops — usefoldl',modify', and strict data.- Lazy maps storing thunks — use
Data.Map.StrictandinsertWithfrom it. IORef/TVarupdates withmodifyIORef— usemodifyIORef'.- Holding on to the head of a huge lazy list while traversing it twice.