4

I'm new to Haskell and reading Haskell from first principles, in chapter Folds page 384 I have came across FoldR and seems like its not tail recursive

foldr :: (a -> b -> b) -> b -> [a] -> b
foldr f z [] = z
foldr f z (x:xs) = f x (foldr f z xs)

1- can we make it tail recursive ?

2- and do it will be optimized ?

Will Ness
  • 70,110
  • 9
  • 98
  • 181
hanan
  • 1,768
  • 1
  • 14
  • 19
  • 4
    Not sure why folks are downvoting, as it's a perfectly reasonable question. But, as the answers have correctly pointed out, "tail recursion" isn't really that relevant in Haskell. You're more worried about storage space for thunks (which is the big reason `foldl` is objectively worse than `foldl'`, for instance, despite it being possible to implement both tail recursively in a straightforward way. – Silvio Mayolo Apr 27 '21 at 23:21
  • Don't know either why this was downvoted - and I don't think it's really a duplicate of the performance question given. IMHO this question and the answers are good and stand on their own so I'd like to vote to reopen this. Sadly that would instant-reopen it and I don't want to turn this into close/reopen war so what do others think? – Random Dev Apr 28 '21 at 07:02
  • 1
    @Carsten the answers do seem rather different here. – Will Ness Apr 28 '21 at 09:38

4 Answers4

7

In a lazy language like Haskell, tail recursion is often a bad idea. This is one of these cases.

We might attempt to make foldr tail recursive by e.g. starting with a reverse (which is also doable in a tail-recursive way), and then accumulating the foldr result step by step, in a tail recursive way. However, that would break the foldr semantics.

With the standard foldr, we have e.g.

foldr (\_ _ -> k1) k2 (x:xs) = k1

no matter what xs is, including a bottom value like undefined or an infinite list like [0..]. Further, when xs is a finite but long list, the code above is also efficient, since it stops the computation immediately without scanning the whole list.

As a more practical example,

and :: [Bool] -> Bool
and = foldr (&&) True

makes and xs return False as soon as some element of xs evaluates to False, without scanning the rest of the list.

Concluding, turning foldr into a tail recursive function would:

  • change the semantics when dealing with partially-defined lists (1:2:undefined) or infinite ones ([0..]);
  • be less efficient on finite length lists, which would always have to be completely scanned, even when there is no need to.
chi
  • 111,837
  • 3
  • 133
  • 218
  • https://stackoverflow.com/a/56958943/3862698 here they have defined foldr using continuation (to me it like returning a function) is its optimized to use it in haskell ? or it will break the semantics as well ? – hanan Apr 27 '21 at 21:44
  • seems like it will break – hanan Apr 27 '21 at 21:46
  • but still it would be optimized ? if we don't include bottom and we have strict function passing inside FoldR ? – hanan Apr 27 '21 at 21:49
  • 1
    @hanan what do you think "optimized" means? – Carl Apr 27 '21 at 22:05
  • 7
    @hanan You are comparing apples and oranges. My answer considers a __lazy__ language like Haskell, while the answer you mention is about F# which is _eager_, so the semantics is different. Because of this difference, we can't apply the same reasoning to both languages: good practices in one language can easily be bad practices in the other one, and vice versa. – chi Apr 27 '21 at 22:17
  • 1
    ^^^ specifically, this "CPS" technique both in F# and Haskell first build an depth-n closure from the n-long list, then applies it to the initial value (called `acc` in the answer you linked). then, in a _strict_ language, a chain of n applications is performed building the result from bottom to the top, which is OK. but in a lazy language the application `cont {func x r}` won't reduce (i.e. evaluate) the application `func x r` -- instead it will be passed as is to `cont`. the effect is two more extraneous traversals, one of which is on stack, with a potential for stack overflow. ... @hanan – Will Ness Apr 29 '21 at 15:21
  • 1
    ... the remedy is to use `(\ r -> cont $! func x r)` as the continuation at each step, to get the needed strictness back. @hanan – Will Ness Apr 29 '21 at 15:21
5

foldr is not tail recursive ... but it can be used to write functions that process lists in constant space. chi already pointed out that it can implement and efficiently. Here's how it can implement an efficient function for summing a list:

mySum :: Num a => [a] -> a
mySum xs = foldr go id xs 0
  where
    go x r acc = r $! x + acc

How's this work? Consider mySum [1,2,3]. This expands to

foldr go id [1,2,3] 0
==> -- definition of foldr
go 1 (foldr go id [2,3]) 0
==> -- definition of go
foldr go id [2,3] $! 1 + 0
==> -- strict application
foldr go id [2,3] 1

We've reduced the list size by one, and haven't accumulated anything on the "stack". The same process repeats till we get

foldr go id [] 6
==> definition of foldr
id 6
==> definition of id
6

Note: if this code is compiled by GHC with optimizations enabled (-O or -O2), then it will actually transform it into blazing-fast tail-recursive code without any further assistance from you. But even unoptimized, it will work okay and not burn up a bunch of memory/stack.

dfeuer
  • 48,079
  • 5
  • 63
  • 167
2

foldr is not tail recursive. Sometime it is called the true "recursive" fold while fold left is "iterative" (because of tail recursion it is equivalent to iteration).

Please note that in Haskell, because of laziness, also foldl do not guarantee constant space, that is why it exists foldl'

1

foldr is not tail recursive itself, but depending on f, it is possible that foldr f is tail recursive. Tail recursion in Haskell is quite subtle because of lazy evaluation.

For example, consider f = (&&). In this case, we have

foldr (&&) acc lst =
   case lst of
      [] -> acc
      (x:xs) -> x && foldr (&&) acc xs
= 
   case lst of
      [] -> acc
      (x:xs) -> if x
                then foldr (&&) acc xs
                else False
=
   case lst of
      [] -> acc
      (x:xs) -> case x of
         True -> foldr (&&) acc xs
         False -> False

Note that in this case, we clearly see that foldr (&&) is tail-recursive. In fact, foldr (||) is also tail recursive. Note that the fact that foldr (&&) is tail recursive is fundamentally because of laziness. If it weren't for laziness, we would have to evaluate foldr (&&) acc xs before substituting the result into x && foldr (&&) acc xs. But because of laziness, we evaluate x first and only then determine whether we need to call foldr (&&) acc xs, and whenever we make that call, it's a tail call.

However, in most cases, foldr f will not be a tail recursive function. In particular, foldr ((+) :: Int -> Int -> Int) is not tail recursive.

Mark Saving
  • 1,752
  • 7
  • 11
  • 1
    I believe in almost all actual applications, `foldr c n` actually *will* be tail recursive, or at least (approximately) tail recursive modulo cons. There are exceptions, like `data SList a = SCons !a !(SList a) | SNil; fromList = foldr SCons SNil`, where it's reasonable for it not to be, but that sort of thing doesn't show up very often. – dfeuer May 24 '21 at 18:19