すまんすまん、理由が分かった、と思う。

length 関数は引数が [] にマッチするか (_:xs) にマッチするかだけを見ている。

その引数である lines 関数は、その内部で
文字列を \n の前と後で分けるために break 関数を使っている。

その break 関数の型は、次の定義になっている。

break :: (a -> Bool) -> [a] -> ([a],[a])
break _ xs@[] = (xs, xs)
break p xs@(x:xs')
    | p x = ([],xs)
    | otherwise = let (ys,zs) = break p xs' in (x:ys,zs)

ここで、break 関数の戻り値てであるタプルの第1要素の「内容」が必要であれば、
in (x:ys,zs) の部分の x:ys によって、メモリがどんどん使われる。

しかし、今回は内容は一切使われていない。

と言うのも、lines 関数の中では break 関数の戻り値であるタプル (x:y) を
x:y というリストに変換して返している。
このリストは length 関数の引数に渡されるが、
リストという形になっているかどうかしか見ていないため、
break 関数の x:ys が使われていない。

よって、文字列の全てをスキャンすることはするが、
スキャンした結果の文字列はメモリに保存されてはいないため、
結果してメモリ消費量が抑えられたと思われる。

文字で説明するのは難しいな。
length、lines、break それぞれの関数をよく見れば、分かると思う。