Skip to content
AgileLogics
7 min read

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, lazy State, and Writer accumulating over long loops — use foldl', modify', and strict data.
  • Lazy maps storing thunks — use Data.Map.Strict and insertWith from it.
  • IORef/TVar updates with modifyIORef — use modifyIORef'.
  • Holding on to the head of a huge lazy list while traversing it twice.