Neither foldl
nor foldr
can do this particular job efficiently (unless you "cheat" by pattern matching on the list as you fold over it), though foldr
can do it a bit less badly. No, what you really need is a different style of fold, sometimes called para
:
para :: (a -> [a] -> b -> b) -> b -> [a] -> b
para _f n [] = n
para f n (a : as) = f a as (para f n as)
para
is very similar to foldr
. Each of them takes a combining function and, for each element, passes the combining function that element and the result of folding up the rest of the list. But para
adds something extra: it also passes in the rest of the list! So there's no need to reconstruct the tail of the list once you've reached the replacement point.
But ... how do you count from the beginning with foldr
or para
? That brings in a classic trick, sometimes called a "higher-order fold". Instead of para go stop xs
producing a list, it's going to produce a function that takes the insertion position as an argument.
(!!=) :: [a] -> (Int, a) -> [a]
xs0 !!= (i0, new) = para go stop xs0 i0
where
-- If the list is empty, then no matter what index
-- you seek, it's not there.
stop = \_ -> error "Index not in the list"
-- We produce a function that takes an index. If the
-- index is 0, we combine the new element with "the rest of the list".
-- Otherwise we apply the function we get from folding up the rest of
-- the list to the predecessor of the index, and tack on the current
-- element.
go x xs r = \i -> case i of
0 -> new : xs
_ -> x : r (i - 1)
Note that para
is easily powerful enough to implement foldr
:
foldr c = para (\a _ b -> c a b)
What's perhaps less obvious is that foldr
can implement a (very inefficient version of) para
:
para f n = snd . foldr go ([], n)
where
go x ~(xs, r) = (x : xs, f x xs r)
Lest you get the wrong idea and think that para
is "better than" foldr
, know that when its extra power isn't needed, foldr
is simpler to use and will very often be compiled to more efficient code.