I would like to compute the A(3, 20)
value of Ackermann function (see Wikipedia) which should be 2^23 - 3 = 8388605
using Data.MemoCombinators
. My code is:
{-# LANGUAGE BangPatterns #-}
import Data.MemoCombinators as Memo
ack = Memo.memo2 Memo.integral Memo.integral ack'
where
ack' 0 !n = n+1
ack' !m 0 = ack (m-1) 1
ack' !m !n = ack (m-1) $! (ack m (n-1))
main = print $ ack 3 20
But it ends up on stack overflow error ;-) Can it be tuned or the computation chain is really that long and even memoization cannot help?
One of the points of the Ackermann function is that computing it recursively leads to a very deep recursion.
The recursion depth is about equal to the result (depending on how you count, it's a few levels more or less) without meoisation. Unfortunately, memoisation doesn't buy you much if you fill the memo table according to the call-tree.
Let's follow the computation of
ack 3 2
:So when you need to calculate a new (not-yet-known)
ack 1 n
, you need to compute two newack 0 n
, and when you need a newack 2 n
, you need two newack 1 n
, and hence 4 newack 0 n
, that's all not too dramatic.But when you need a new
ack 3 n
, you needack 3 (n-1) - ack 3 (n-2)
newack 2 k
. All things told, after you computedack 3 k
, you need to compute2^(k+2)
new values ofack 2 n
, and by the calling structure, these are nested calls, so you get a stack of2^(k+2)
nested thunks.To avoid that nesting, you need to restructure the computation, e.g. by forcing the new needed
ack (m-1) k
in increasing order ofk
,which allows the computation to run (slowly) with a small stack (but it needs a terrible lot of heap still, a tailor-made memoisation strategy seems called for).
Storing only
ack m n
form >= 2
, and evaluatingack 1 n
as if it were memoised reduces the necessary memory far enough that computingack 3 20
finishes using less than 1GB of heap (usingInt
instead ofInteger
makes it run about twice as fast):