One point that isn't covered explicitly in the other answer, is that there is a sense in which laziness means "even if you map something over a list several times and filter it several times, it will only pass over the list once".
Lets look what happens when we evaluate map foo (map foo [1, 2, 3]) but rather than the consumer being head as in Willem Van Onsem's answer, let's say we're printing it so we need to evaluate the whole thing.
Although I don't want to trace the actual evaluation of print in much detail, so let's pretend we've got a special-purpose print for lists that looks something like this (and that putStr is a primitive):
print :: Show a => [a] -> IO ()
print [] = putStr "[]"
print (x : xs) = putStr "[" >> putStr (show x) >> print_rest xs
where print_rest [] = putStr "]"
print_rest (x : xs) = putStr ", " >> putStr (show x) >> print_rest xs
And of course we have:
map :: (a -> b) -> [a] -> [b]
map _ [] = []
map f (x:xs) = f x : map f xs
And:
foo p = p * 2
So, we start with:
print (map foo (map foo [1, 2, 3]))
Which is of course really:1
print (map foo (map foo (1 : 2 : 3 : []) ))
print needs to pattern match its argument, which requires forcing the outer map. Outer map in turn starts by needing to know whether its argument is empty or not, and thus forces the inner map call. The inner map also needs to pattern match its argument, but that is a concrete list that is non-empty. So now we can return up the pattern match stack and get this:
print (map foo (map foo (1 : 2 : 3 : []) ))
print (map foo (foo 1 : map foo (2 : 3 : []) ))
print (foo (foo 1) : map foo (map foo (2 : 3 : []) ))
putStr "[" >> putStr (show (foo (foo 1))) >> print_rest (map foo (map foo (2 : 3 : []) ))
Now we've got something the IO driver can work with (a putStr call). So it can onsume that, resulting in the console output showing:
[
And we are left with the expression:
putStr (show (foo (foo 1))) >> print_rest (map foo (map foo (2 : 3 : []) ))
Which has another putStr at the head; this one just needs to force show (foo (foo 1)). show has to force foo (foo 1), which has to force foo 1 to 2; then the outer foo can produce 4 so show can produce "4".
putStr "4" >> print_rest (map foo (map foo (2 : 3 : []) ))
Letting IO consume that too we now have on the console output:
[4
And are left with this expression:
print_rest (map foo (map foo (2 : 3 : []) ))
By the same process as above we can go:
print_rest (map foo (map foo (2 : 3 : []) ))
print_rest (map foo (foo 2 : map foo (3 : []) ))
print_rest (foo (foo 2) : map foo (map foo (3 : []) ))
putStr "," >> putStr (show (foo (foo 2))) >> print_rest (map foo (map foo (3 : []) ))
putStr "," >> putStr (show (foo 4)) >> print_rest (map foo (map foo (3 : []) ))
putStr "," >> putStr (show 8) >> print_rest (map foo (map foo (3 : []) ))
putStr "," >> putStr "8" >> print_rest (map foo (map foo (3 : []) ))
Then the IO driver can consume the putStr calls so we an see:
[4, 8
And are left with the expression to evaluate:
print_rest (map foo (map foo (3 : []) ))
Which becomes (skipping over some steps now):
putStr "," >> putStr (show (foo (foo 3))) >> print_rest (map foo (map foo [] ))
putStr "," >> putStr "12" >> print_rest (map foo (map foo [] ))
IO does it magic thing so that we can see on the console:
[4, 8, 12
And finally the residual print_rest (map foo (map foo [] )) evaluates very straightforwardly to putStr "]" so we at last can see:
[4, 8, 12]
Now, let's reflect on what happened.
Lazy evaluation means we don't evaluate anything until it's needed, and the "need" ultimately comes from the IO driver needing to evaluate things until it has concrete primitives with fully evaluated arguments so that it can actually do something. That's why I chose the order that I did to evaluate things.
If you look over it you should notice that at no point did we ever produce an expression like map foo [2, 4, 6]. We evaluated both of the foo calls on the first element of the list before any of the mapping or printing even pattern matched to see if there was a second element of the list. This also means that the first element of the list (and both results of fooing it) became unreferenced and could be reclaimed by the garbage collector before the second element of the list was examined. And then the second element of the list was fully processed before the third was examined, too.
This is the sense in which laziness evaluates nested maps, filters, etc with only a single traversal of the base list. Sometimes this can result in large efficiency gains compared to an eager evaluation of the same nested maps, filters, etc. For example, if the base list was not a small concrete list like [1, 2, 3] but rather an expression that would lazily produce a very large (even infinite!) list, then our whole "multi-pass" sequence of maps could operate with only enough memory for one element at a time, rather than needing to produce the full base sequence, and then produce the full sequence at the next stage, and so on for each stage. It also means that we produce and consume all the intermediate stages of one value immediately, increasing the chance that we are working within the CPU cache or the shortest-lived most-efficient generation of the garbage collector's heap.
However, if you look very carefully, you'll note that even though we "only traversed the list once", we still had exactly the same : constructor applications that we would have had if we had fully evaluated map foo [1, 2, 3] to [2, 4, 6] and then traversed that. We still allocated a constructor cell for foo 1 : _, then immediately consumed it and allocated foo 2 : _.
So in another sense "laziness means we only traverse the list once" is not true. When we do end up using the whole list (as opposed to using head or take to only inspect some of it), then lazy evaluation results in exactly the same work (meaning allocations and pattern matches) as if we did evaluate each stage in its entirety and then traverse the result for the next stage. We've rearranged the work, and the order produced by lazy evaluation is frequently better, but we haven't fundamentally changed the amount of work we have to do. The intermediate lists are still there in some sense, even if they're never present in memory all at once.
Fortunately GHC has many other optimizations that come into play to avoid actually creating the intermediate list cells (or indeed the base list!), in many cases. For example, GHC can recognise that map foo (map bar xs) is equivalent to map (foo . bar) xs, which has no intermediate list to traverse or allocate whether we're evaluating lazily or eagerly. Particularly simple cases like the example I've used would be heavily inlined, and probably end up essentially compiled to just directly print [4, 8, 12] without allocating or evaluating anything.
TLDR: There is a sense in which lazy evaluation does avoid repeatedly traversing intermediate lists when you chain functions like maps and filters. So the quote in the OP may well be accurate (depending on how its author intended it), but there's important nuance and it's important not to oversell it.
When the entirety of the result is needed, all the work of those traversals is still done; lazy evaluation just rearranges the order the work is done in. Which could still be a big win if it allows you to have dramatically less memory consumed by intermediate results, since they now may not have to all be present in memory at once.
1 Remember that square-bracket syntax for lists is just a nice way for humans to write out a sequence that is really represented in memory as a nested series of applications of the : constructor, terminated by the [] constructor. [1, 2, 3] is 1 : (2 : (3 : [])), and we don't need those inner parentheses because the : constructor is infix and right associative, so we can just write 1 : 2 : 3 : [].