As already mentioned in the comments, your lists are associating in the worst possible way. If we look at the usual (and most sensible) definition of (++)
, we see
(++) :: [a] -> [a] -> [a]
[] ++ ys = ys
(x:xs) ++ ys = x : (xs ++ ys)
(Source)
So (++)
is O(n)
where n
is the length of the first list only. It doesn't depend on the length of the second. When you do foldl1
(or any of the other foldl
variants), the derivation looks something like...
foldl1 (++) [a, b, c, d]
==> ((a ++ b) ++ c) ++ d
Since we only look at the first argument to determine the complexity, if we let n
be the sum of the lengths of a
, b
, c
, d
, then we're iterating over the elements of a
three times, those of b
twice, and c
once. So we're doing, ostensibly, n
operations n
times, for a total of O(n^2)
operations. (You can do the math of this rigorously. You'll end up with a sum resulting in a triangular number, and the triangular number formula is quadratic, which is where the n^2
comes from. Proof is left as an exercise to the reader.)
On the other hand, if we use foldr
, then
foldlr (++) [] [a, b, c, d]
==> a ++ (b ++ (c ++ (d ++ [])))
Again, we only look at the left-hand argument every time. In this case, the arguments are organized nicely, so we only iterate over each element once, resulting in O(n)
recursive steps.
Difference lists solve this in a different way, by "magically" reassociating your parentheses to be better placed. You can read more about those in the linked article.
The reason concat
works better is that the people who wrote Haskell knew about these problems and knew to use foldr
(or equivalent) to get the better performance.