为何这段Haskell代码内存占用持续增长直至崩溃?
Great question—let’s unpack exactly why your crash function is eating up memory until it crashes, even though you’re using foldl' (the strict fold you’d expect to avoid leaks).
The Core Issue: Weak Head Normal Form (WHNF) vs. Full Evaluation
You’re right that foldl' replaces the accumulator each step and evaluates it to Weak Head Normal Form (WHNF). But here’s the catch: WHNF only evaluates the outermost constructor of a value, not its inner components.
Your accumulator is a tuple (Int, Int). When foldl' evaluates this tuple to WHNF, it just confirms "this is a tuple"—it doesn’t force evaluation of the two Int values inside the tuple. Those Ints can remain as unevaluated thunks (pending computations), and that’s where your leak happens.
Let’s Trace the Thunk Build-Up
Look at how next is computed in your spin function:
next | indexNIncrement >= spinNumber = 1 + (indexNIncrement `rem` spinNumber) | otherwise = 1 + indexNIncrement
- In the first branch,
remis a strict operation—it forcesindexNIncrement(and thusindex + increment) to be fully evaluated. No thunk builds up here. - In the second branch,
nextbecomes1 + index + increment—this creates a new thunk. Sincefoldl'only evaluates the tuple constructor (not theIntinside), this thunk becomes the newindexin your accumulator.
With 50 million iterations, if most steps hit the second branch, you end up with a massive chain of nested thunks like 1 + (1 + (1 + (... + increment)...)). Each thunk references the previous one, so none can be garbage collected until the entire fold completes. By then, you’ve already eaten up all available memory.
Fixing the Leak: Force Full Evaluation of Accumulator Components
To stop the thunk build-up, you need to ensure the Int values in your accumulator are fully evaluated at each step. Here are two straightforward ways:
1. Use Bang Patterns (Simplest Approach)
Enable the BangPatterns extension to mark the accumulator’s components as strict. This forces Haskell to evaluate them before proceeding:
{-# LANGUAGE BangPatterns #-} crash :: Int -> Int -> Int crash increment spins = snd $ foldl' spin' (0,0) [1..spins] where spin' = spin increment spin increment !(!index, !element1) spinNumber = (next, nextElementOne) where next | indexNIncrement >= spinNumber = 1 + (indexNIncrement `rem` spinNumber) | otherwise = 1 + indexNIncrement indexNIncrement = index + increment nextElementOne | next == 1 = spinNumber | otherwise = element1
The ! symbols tell GHC to fully evaluate index and element1 before running the rest of spin.
2. Use seq for Manual Strictness
If you prefer not to use extensions, you can use seq to force evaluation of the new accumulator values before returning them:
crash :: Int -> Int -> Int crash increment spins = snd $ foldl' spin' (0,0) [1..spins] where spin' = spin increment spin increment (index, element1) spinNumber = next `seq` nextElementOne `seq` (next, nextElementOne) where next | indexNIncrement >= spinNumber = 1 + (indexNIncrement `rem` spinNumber) | otherwise = 1 + indexNIncrement indexNIncrement = index + increment nextElementOne | next == 1 = spinNumber | otherwise = element1
seq a b evaluates a to WHNF before returning b. Here, we ensure both next and nextElementOne are fully evaluated before packing them into the tuple.
Why Your Initial Assumption Was Off
You wondered why the old accumulator values aren’t freed—because the new accumulator’s thunk references the old one. Until the thunk is evaluated, the garbage collector can’t clean up those previous values. With strict evaluation of the accumulator components, each old value is no longer referenced by the new thunk, so it gets collected immediately.
内容的提问来源于stack exchange,提问作者Quinn Wilson

