Using Data.Memocombinators
Again, note how ack is defined as the memoized version of ack' and ack' calls ack for recursive cases.
Using Array memoization
Faster results can be obtained by using arrays to memoize the functions ack(1,.), ack(2,.) and ack(3,.):
import Data.Array
import Data.Ix
-- memoize the function f using arrays
arrayMemo bnds f = g
where g i = arr ! i
arr = array bnds [ (i,f arr i) | i <- range bnds ]
a0 n = n+1
a1 = arrayMemo (0,530000) f
where f arr 0 = a0 1
f arr n = a0 (arr ! (n-1))
a2 = arrayMemo (0,270000) f
where f arr 0 = a1 1
f arr n = a1 (arr ! (n-1))
a3 = arrayMemo (0,16) f
where f arr 0 = a2 1
f arr n = a2 (arr ! (n-1))
The only drawback is that explicit bounds have to be determined for each level function. This approach computes a3 16 in about half a second.