2

I know that tail recursion in Haskell is subject to some difficulties due to its being lazy. That is, is it sensible to use tail recursion in Haskell?

Incerteza
  • 32,326
  • 47
  • 154
  • 261
  • "I know that tail recursion in Haskell is subject to some difficulties due to its being lazy." It is? Could you provide a reference for such a claim? – Tom Ellis Nov 03 '13 at 11:02
  • @TomEllis: The problem is that lazyness can cause things like foldl and similar hand-built recursive loops to leak space. – hugomg Nov 03 '13 at 16:24

2 Answers2

7

The answer, as usual with these big questions, is "it depends".

When you're programming in a lazy style, you can often get away with naive recursion, for example

 map f (x:xs) = f x : map f xs
 map _ []     = []

Is actually how map is defined in the standard libs. And this is good, since a tail recursive function yields the result generally can't be nicely lazy, a tail recursive map won't terminate if you did something like head . map (+1) $ [1..].

However, sometimes we don't want to be lazy. The classic example is when we're too lazy and start to build up thunks that we really just want evaluated, like when summing a list.

sum xs = someFold (+) 0 xs

Now since + is strict in it's arguments and associative, there's no reason to use foldr but foldl' is perfect, it's tail recursive and evaluates strictly which gives noticeable space improvements. The ' is important, in many tail recursive functions, there's an accumulator that's modified on each recursive step, foldl' will force it's evaulation which prevents our tail recursive function from being too lazy and building up thunks.

So the moral of the story, when you're programming lazily and producing lazy datastructures, tail recursion isn't as big a deal. But when you're programming strictly and using functions strict in both arguments, then tail recursion is very important for maintaining good performance.

daniel gratzer
  • 52,833
  • 11
  • 94
  • 134
  • I wonder, isn't `head . map (+1) $ [1..]` the same as `head (map (+1) $ [1..] x)`? If yes, why do you use 1) $ 2) funct composition? 3) [1..] ? Shouldn't it be just `head $ map (+1) [1..]`? – Incerteza Nov 03 '13 at 12:28
  • `f . g $ x` is the same as `f $ g $ x` is the same as `f (g x)`. Some people prefer the first form because it emphasizes building a functional pipeline then applying the argument at the end with `$`. – J. Abrahamson Nov 03 '13 at 14:21
  • @Alex I tend to write functions as `compose . as . many . as . possible $ apply` it's a style choice – daniel gratzer Nov 03 '13 at 14:22
  • Composition is associative. It makes certain refactorings easier as you can cut a part anywhere in the middle of a long pipeline and name it. – nponeccop Nov 03 '13 at 18:35
7

Tail recursion isn't as straightforward or as big of a deal in Haskell as it is in strict languages. Usually, instead, you should aim to write productive functions. For instance, foldr is often productive

foldr f z []     = z
foldr f z (x:xs) = x `f` foldr f z xs

If the combining function f is able to produce a partial result lazily then whatever is consuming the results from foldr can demand them lazily as well. A typical example is "foldr identity"

foldr (:) [] [1,2,3]          -- "force" it once
1 : {{ foldr (:) [] [2,3] }}

where the {{...}} is a lazy thunk. If the calling context of this foldr is, say, head then we're done

head (foldr (:) [] [1,2,3])
head (1 : {{ foldr (:) [] [2,3] }})
1

However, if the f in foldr is strict then foldr can create linearly many calling frames

foldr (+) 0 [1,2,3]
1 + {{ foldr (+) 0 [2,3] }}           -- we know it's one more than *something*
1 + (2 + {{ foldr (+) 0 [3] }})       -- ...
1 + (2 + (3 + {{ foldr (+) 0 [] }}))
1 + (2 + (3 + 0))                     -- and now we can begin to compute
1 + (2 + 3)
1 + 5
6

While foldl' lets the strict combining function operate immediately

foldl' f z []     = z
foldl' f z (x:xs) = let z' = f z x in z' `seq` foldl' f z' xs

where that seq noise forces Haskell to evaluate like this

foldl' (+) 0 [1,2,3]
foldl' (+) (1+0) [2,3]
foldl' (+) 1 [2,3]
foldl' (+) (1+2) [3]
foldl' (+) 3 [3]
foldl' (+) (3+3) []
foldl' (+) 6 []
6

Which looks a lot like a tail recursive call.

See the wiki for a bit more detail.

J. Abrahamson
  • 72,246
  • 9
  • 135
  • 180
  • How do you decide whether to make a function tail recursive or just productive? – CMCDragonkai Oct 13 '15 at 12:47
  • 1
    Generally if something has streaming behavior or takes advantage of laziness then productivity is better. When something is strict and needs to run in constant space then tail recursion is better. – J. Abrahamson Oct 13 '15 at 13:35