6 min read
On this page

Strictness

Lazy by default is a sensible choice for most code. But in places where you know the value will be needed, where the data is large, or where you're accumulating in a tight loop, laziness becomes a liability. Haskell gives you a handful of tools to demand evaluation: seq, bang patterns, strict data fields, and strict variants of standard data structures. Knowing when to reach for each is what separates Haskell that runs in 100MB from Haskell that runs in 100GB doing the same job.

seq: The Primitive

seq is the core strictness primitive:

seq :: a -> b -> b

Read it as: "evaluate a to weak head normal form, then return b." The value of seq x y is y, but with the guarantee that x was forced first.

let big = sum [1..1000000]
in big `seq` putStrLn "done"

Without seq, big would be a thunk that nothing ever forces (since putStrLn doesn't use it). With seq, GHC evaluates sum [1..1000000] before printing.

seq only goes one level deep — to weak head normal form (WHNF). For a tuple, it forces the tuple constructor but not the contents:

let pair = (1 + 1, 2 + 2)
in pair `seq` ...   -- forces (,) but not 1+1 or 2+2

For deep evaluation, use deepseq from the deepseq package:

import Control.DeepSeq (deepseq, force, NFData)

let xs = map (* 2) [1..1000]
in xs `deepseq` ...   -- forces every element

deepseq requires an NFData instance for the type. Most standard types have one; for your own types, derive it:

{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE DeriveAnyClass #-}

import GHC.Generics (Generic)
import Control.DeepSeq (NFData)

data User = User
  { userId   :: Int
  , userName :: String
  } deriving (Generic, NFData)

Bang Patterns

BangPatterns is a language extension that lets you put ! in front of a pattern to force evaluation when the pattern is matched:

{-# LANGUAGE BangPatterns #-}

go :: Int -> [Int] -> Int
go !acc []     = acc
go !acc (x:xs) = go (acc + x) xs

Each call to go forces acc to WHNF before doing anything else. This prevents the accumulator from being a thunk chain.

This is essentially syntactic sugar for seq:

-- These are equivalent
go acc xs = case acc of
  !_ -> case xs of
    []     -> acc
    (y:ys) -> go (acc + y) ys

go !acc []     = acc
go !acc (x:xs) = go (acc + x) xs

The bang version is shorter and idiomatic. You'll see it in nearly every production Haskell codebase, especially in loops, fold helpers, and state machines.

$!: Strict Application

$! is strict function application:

($!) :: (a -> b) -> a -> b
f $! x = x `seq` f x

f $! x forces x before applying f. This is useful when you want to force an argument at the call site without changing the function:

-- Force the result of expensive before passing it
print $! sum [1..1000000]

Compare with $ (lazy application), which doesn't force anything. In practice, $! is less common than bang patterns because most strictness lives in function definitions, not call sites. But it's handy for one-off forcing.

Strict Data Fields

Haskell record fields are lazy by default. A field can hold a thunk indefinitely. To make a field strict, use !:

data User = User
  { userId   :: !Int           -- strict
  , userName :: !String        -- strict
  , userBio  :: String         -- lazy
  }

When you construct a User, the strict fields are forced to WHNF. The lazy field remains a thunk.

For numeric fields and small fixed-shape data, make every field strict by default. The downsides of laziness (thunks holding references, space leaks) are real; the upsides (deferred computation) are rare for fields like Int, Bool, or Double.

For very large codebases, the StrictData language extension makes all fields in a module strict unless explicitly marked lazy with ~:

{-# LANGUAGE StrictData #-}

data User = User
  { userId   :: Int       -- strict, even without !
  , userBio  :: ~String   -- explicitly lazy
  }

Mercury uses StrictData module-wide for most of its codebase. IOG/Cardano teams do the same. The convention is "strict by default, lazy when you need it," which inverts Haskell's default but matches the common case.

Data.Map.Strict vs Data.Map

Data.Map is the standard balanced binary tree map. It comes in two flavors:

  • Data.Map (Data.Map.Lazy): values can be thunks.
  • Data.Map.Strict: values are forced when inserted.

The keys are always evaluated (to compute the order). The difference is in the values. Consider:

import qualified Data.Map.Strict as Map
import qualified Data.Map        as Map.Lazy

count :: [String] -> Map.Map String Int
count = foldr step Map.empty
  where
    step word m = Map.insertWith (+) word 1 m

With Data.Map.Strict, the (+) is applied and the result forced before being stored. With Data.Map, you might end up with a value like 1 + 1 + 1 + 1 + 1 + ... as a thunk chain inside the map. For a frequency counter on millions of inputs, that's a guaranteed space leak.

Rule of thumb: use Data.Map.Strict unless you specifically need lazy values. The same applies to Data.IntMap.Strict vs Data.IntMap, and Data.HashMap.Strict vs Data.HashMap.Lazy from unordered-containers.

Data.Set only has one variant — it's effectively a Map a () and the () value is always forced trivially.

When to Be Strict

A few rules that hold up across real codebases:

Numeric accumulators in folds. Always strict. foldl' does this for you; for hand-written recursion, use bang patterns.

Fields of small records. Strict. Int, Double, Bool, fixed-size enums — make them strict. There's almost never a reason for a Bool field to be a thunk.

Map and Set values for aggregation. Strict map (Data.Map.Strict).

Long-running state in event loops. Strict. Force the state on each iteration.

Large data structures held in memory for the lifetime of the program. Use deepseq or NFData to force them when they're loaded, then they stay forced.

Fields that are sometimes-computed, sometimes-skipped. Lazy. If a field represents an expensive computation that may not be needed, leave it lazy. This is the classic justification for laziness on records.

Lists you'll consume in a streaming way. Lazy. Forcing a list eagerly defeats the purpose.

foldl' as the Canonical Example

foldl' is what you get when you take foldl and add strictness:

foldl' :: (b -> a -> b) -> b -> [a] -> b
foldl' f z []     = z
foldl' f z (x:xs) = let z' = f z x
                    in z' `seq` foldl' f z' xs

The seq forces the new accumulator before the recursive call. That's the whole difference. Without it, you have foldl, which builds a thunk chain and blows the stack. With it, you have constant-space iteration.

This pattern — "compute the next state, force it, recurse" — is the most common strictness idiom in Haskell. Whenever you write a hand-rolled loop, copy this shape:

{-# LANGUAGE BangPatterns #-}

processEvents :: State -> [Event] -> State
processEvents !s []     = s
processEvents !s (e:es) = processEvents (handle s e) es

The !s ensures the state is forced before each step. If State is a record, also make its fields strict (or use StrictData).

A Worked Example: Mean of a Stream

A naive mean:

-- Bad: leaks memory on long inputs
mean :: [Double] -> Double
mean xs = sum xs / fromIntegral (length xs)

This is fine for small lists. For a million-element list, sum and length each walk the list, but laziness means the list itself can be discarded between the walks. The bigger problem is using length at all if the input is a stream — you'd need to buffer it.

A single-pass strict version:

{-# LANGUAGE BangPatterns #-}
import Data.List (foldl')

data MeanState = MeanState !Int !Double

mean :: [Double] -> Double
mean xs = case foldl' step (MeanState 0 0) xs of
  MeanState 0 _   -> 0
  MeanState n tot -> tot / fromIntegral n
  where
    step (MeanState !n !tot) x = MeanState (n + 1) (tot + x)

The !Int and !Double fields combined with foldl' mean this runs in constant memory regardless of list length.

This is the bread-and-butter pattern for streaming analytics in Haskell. Sigma at Facebook does roughly this for ad-event aggregation; financial codebases at Standard Chartered use it for tick processing.

Common Pitfalls

Bang pattern on the wrong argument. go !x !y = ... forces both. go x !y = ... forces only y. If your loop has multiple state arguments, make sure each one that needs forcing has a !.

Strict field but lazy value passed in. User !Int !String doesn't help if you write User someThunk someOtherThunk. The fields are forced during construction, but if the thunks are large nested expressions, you've still allocated them. Force at the source if needed.

Using Data.Map (lazy) for accumulating counts. This is the most common Haskell space leak in the wild. Every time you see Map.insertWith (+), ask whether it's the strict map.

Over-forcing and slowing things down. deepseq-ing every value "just in case" forces work that the program would otherwise skip. Profile to find leaks; don't preemptively force.

Assuming seq deeply evaluates. xs seq ... only forces the first cons cell of xs. The elements remain thunks. If you need every element evaluated, use deepseq.

Forgetting BangPatterns is an extension. It's commonly enabled but not on by default. You'll get a parse error if you use ! patterns without enabling it.

Key Takeaways

Strictness in Haskell is opt-in, and you opt in with seq, bang patterns, strict fields, and strict variants of standard data types. The single most important rule: use Data.Map.Strict, not Data.Map, for any aggregating workload. The second: bang-pattern accumulators in any hand-written fold. The third: make fields of small records strict by default, or turn on StrictData module-wide. foldl' is the canonical example — strict left fold, constant space, no surprises. The rest is taste and profiling.