To complement the other answers with a bit of larger perspective: with lazy lists, using foldl'
in a function that returns a list is usually a bad idea. foldl'
is often useful when you are reducing a list to a strict (non-lazy) scalar value (e.g., summing a list). But when you're building a list as the result, foldr
is usually better, because of laziness; the :
constructor is lazy, so the list's tail isn't computed until it's actually needed.
In your case:
newList_foldr lst = foldr (\x acc -> x*2 : acc) [] lst
This is actually the same as map (*2)
:
newList_foldr lst = map (*2) lst
map f lst = foldr (\x acc -> f x : acc) [] lst
Evaluation (using the first, map
-less definition):
newList_foldr [1..10]
= foldr (\x acc -> x*2 : acc) [] [1..10]
= foldr (\x acc -> x*2 : acc) [] (1:[2..10])
= 1*2 : foldr (\x rest -> f x : acc) [] [2..10]
This is about as far Haskell will evaluate when newList [1..10]
is forced. It only evaluates any further if the consumer of this result demands it—and only as little as needed to satisfy the consumer. So for example:
firstElem [] = Nothing
firstElem (x:_) = Just x
firstElem (newList_foldr [1..10])
-- firstElem only needs to evaluate newList [1..10] enough to determine
-- which of its subcases applies—empty list or pair.
= firstElem (foldr (\x acc -> x*2 : acc) [] [1..10])
= firstElem (foldr (\x acc -> x*2 : acc) [] (1:[2..10]))
= firstElem (1*2 : foldr (\x rest -> f x : acc) [] [2..10])
-- firstElem doesn't need the tail, so it's never computed!
= Just (1*2)
This also means that the foldr
-based newList
can also work with infinite lists:
newList_foldr [1..] = [2,4..]
firstElem (newList_foldr [1..]) = 2
If you use foldl'
, on the other hand, you must always compute the whole lists, which also means that you can't work on infinite lists:
firstElem (newList_good [1..]) -- doesn't terminate
firstElem (newList_good [1..10])
= firstElem (foldl' (\acc x -> x*2 : acc) [] [1..10])
= firstElem (foldl' (\acc x -> x*2 : acc) [] (1:[2..10]))
= firstElem (foldl' (\acc x -> x*2 : acc) [2] [2..10])
-- we can't short circuit here because the [2] is "inside" the foldl', so
-- firstElem can't see it
= firstElem (foldl' (\acc x -> x*2 : acc) [2] (2:[3..10]))
= firstElem (foldl' (\acc x -> x*2 : acc) [4,2] [3..10])
...
= firstElem (foldl' (\acc x -> x*2 : acc) [18,16,14,12,10,8,6,4,2] (10:[]))
= firstElem (foldl' (\acc x -> x*2 : acc) [20,18,16,14,12,10,8,6,4,2] [])
= firstElem [20,18,16,14,12,10,8,6,4,2]
= firstElem (20:[18,16,14,12,10,8,6,4,2])
= Just 20
The foldr
-based algorithm took 4 steps to compute firstElem_foldr (newList [1..10])
, whereas the foldl'
-based one took in the order of 21 steps. What's worse is that the 4 steps is a constant cost, whereas the 21 is proportional to the length of the input list—firstElem (newList_good [1..150000])
takes 300,001 steps, while firstElem (newList_foldr [1..150000]
takes 5 steps, as does firstElem (newList_foldr [1..]
for that matter.
Note also that firstElem (newList_foldr [1.10])
runs in constant space as well as constant time (it has to; you need more than constant time to allocate more than constant space). The pro-foldl
truism from strict languages—"foldl
is tail recursive and runs in constant space, foldr
is not tail recursive and runs in linear space or worse"—is not true in Haskell.
acc ++ [x*2]
step in the example) is inefficient. https://mcmap.net/q/183700/-haskell-accessing-lists-from-back/507803 https://mcmap.net/q/183701/-why-can-you-only-prepend-to-lists-in-functional-languages/507803 – Riddick(++)
is O(n),(:)
is O(1). Also(++)
needs the entire list to be evaluated so even though you're using strict fold, you're still building up a lot of thunks. – Technicolor