Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Under what circumstances are monadic computations tail-recursive?

In Haskell Wiki's Recursion in a monad there is an example that is claimed to be tail-recursive:

f 0 acc = return (reverse acc) f n acc = do     v  <- getLine     f (n-1) (v : acc) 

While the imperative notation leads us to believe that it is tail-recursive, it's not so obvious at all (at least to me). If we de-sugar do we get

f 0 acc = return (reverse acc) f n acc = getLine >>= \v -> f (n-1) (v : acc) 

and rewriting the second line leads to

f n acc = (>>=) getLine (\v -> f (n-1) (v : acc)) 

So we see that f occurs inside the second argument of >>=, not in a tail-recursive position. We'd need to examine IO's >>= to get an answer. Clearly having the recursive call as the last line in a do block isn't a sufficient condition a function to be tail-recursive.


Let's say that a monad is tail-recursive iff every recursive function in this monad defined as

f = do     ...     f ... 

or equivalently

f ...  =  (...) >>= \x -> f ... 

is tail-recursive. My question is:

  1. What monads are tail-recursive?
  2. Is there some general rule that we can use to immediately distinguish tail-recursive monads?

Update: Let me make a specific counter-example: The [] monad is not tail-recursive according to the above definition. If it were, then

f 0 acc = acc f n acc = do     r <- acc     f (n - 1) (map (r +) acc) 

would have to be tail-recursive. However, desugaring the second line leads to

f n acc = acc >>= \r -> f (n - 1) (map (r +) acc)         = (flip concatMap) acc (\r -> f (n - 1) (map (r +) acc)) 

Clearly, this isn't tail-recursive, and IMHO cannot be made. The reason is that the recursive call isn't the end of the computation. It is performed several times and the results are combined to make the final result.

like image 654
Petr Avatar asked Nov 14 '12 12:11

Petr


People also ask

What makes a recursive function tail recursive?

A function is tail-recursive if it ends by returning the value of the recursive call. Keeping the caller's frame on stack is a waste of memory because there's nothing left to do once the recursive call returns its value.

What is a tail recursive function Why is tail recursion important?

In tail recursion, there is no other operation to perform after executing the recursive function itself; the function can directly return the result of the recursive call. In simple words, in tail recursion, the recursive function is called last. So it is more efficient than non-tail recursion.

Is Foldl tail recursive?

Because foldl is tail recursive, languages that perform tail call elimination would rewrite it to something that resembles the following. But, again, Elm and JavaScript do not provide us with automatic tail call elimination. It is simple to define a version using Trampoline .

Can everything be tail recursive?

It depends on exactly what you are asking. If you want to keep all functions as functions (no mutable state) with the same signatures, then no. The most obvious example is the quicksort, where both calls can't be tail calls.


1 Answers

A monadic computation that refers to itself is never tail-recursive. However, in Haskell you have laziness and corecursion, and that is what counts. Let's use this simple example:

forever :: (Monad m) => m a -> m b forever c' = let c = c' >> c in c 

Such a computation runs in constant space if and only if (>>) is nonstrict in its second argument. This is really very similar to lists and repeat:

repeat :: a -> [a] repeat x = let xs = x : xs in xs 

Since the (:) constructor is nonstrict in its second argument this works and the list can be traversed, because you have a finite weak-head normal form (WHNF). As long as the consumer (for example a list fold) only ever asks for the WHNF this works and runs in constant space.

The consumer in the case of forever is whatever interprets the monadic computation. If the monad is [], then (>>) is non-strict in its second argument, when its first argument is the empty list. So forever [] will result in [], while forever [1] will diverge. In the case of the IO monad the interpreter is the very run-time system itself, and there you can think of (>>) being always non-strict in its second argument.

like image 132
ertes Avatar answered Sep 25 '22 00:09

ertes