How would you adjust this simple recursion example so that tail call optimization occurs (and not a StackOverflowError
)?
count 0 = 0
count n = succ (count (pred n))
count 100000
The most-common cause of stack overflow is excessively deep or infinite recursion, in which a function calls itself so many times that the space needed to store the variables and information associated with each call is more than can fit on the stack.
A stack overflow is a type of buffer overflow error that occurs when a computer program tries to use more memory space in the call stack than has been allocated to that stack.
When any function is called from main(), the memory is allocated to it on the stack. A recursive function calls itself, the memory for a called function is allocated on top of memory allocated to the calling function and a different copy of local variables is created for each function call.
Recursion is the process of defining a problem (or the solution to a problem) in terms of (a simpler version of) itself. For example, we can define the operation "find your way home" as: If you are at home, stop moving. Take one step toward home. "find your way home".
This is the kind of stack overflows I call the "length/foldr" kind. It occurs, when the recursive function is applied to compute the strict argument of the function application that constitutes the result. Compare:
-- naive computation of list length
-- this is not like it's defined in Frege, of course
length [] = 0
length (_:xs) = 1 + length xs
This also happens with foldr f
when f
is strict in its second argument.
There are other possible reasons for stack overflows (SO):
a
tail calls b
, which tail calls c
, ..., which tail calls a
). This should also never result in a SO in Frege.foldl
in Haskell. In Frege, the standard fold
is strict in the accumulator and hence immune in many cases. However, the following still overflows on long lists: fold (<>) mempty (map (Just . Sum) [1..10000])
Here is an example for the last case:
even 0 = true
even n = case even (pred n) of
true -> false
false -> true
The second equation is semantically equivalent to even n = not (even (pred n))
and thus is an even more malicious variant of 4.
To answer your question, in your example one could use an accumulator to create a tail-recursive version:
count n = go 0 n
where
go acc 0 = acc
go acc n = go (succ acc) (pred n)
It is perhaps worth noting that your example also fails in Haskell:
GHCi, version 7.6.3: http://www.haskell.org/ghc/ :? for help
Loading package ghc-prim ... linking ... done.
Loading package integer-gmp ... linking ... done.
Loading package base ... linking ... done.
Prelude> let count 0 = 0; count n = succ (count (pred n))
Prelude> count 10000
10000
Prelude> count 100000
100000
Prelude> count 1000000
1000000
Prelude> count 10000000
10000000
Prelude> count 100000000
*** Exception: stack overflow
The reason that it overflows only with much higher numbers is that the Haskell RTS manages the RAM in a way that is better suited for functional programming, whereas the JVM allocates a tiny default stack on startup, that accommodates a call depth of a few 1000 at best.
You can compute much greater numbers with your program even in Frege when you force the JVM to allocate bigger stacks:
java -Xss512m ....
Experience shows that a stack of size 512m allows a call depth of approximatly 10 million for single argument functions.
This is, however, not a general solution, because then the JVM creates all threads with this stack size. Thus precious RAM is wasted. And even when you have plenty of RAM, you'll probably not be able to create a stack greater than 2g.
In the next major version of Frege, certain pathological cases of stack overflow kinds 3 and 4 (see above) will be managed better, hopefully. As of today, however, such constructs will work only for small problem sizes that happen to fit the JVM stack.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With