Combine memoization and tail-recursion

前端 未结 5 1153
再見小時候
再見小時候 2020-12-02 14:38

Is it possible to combine memoization and tail-recursion somehow? I\'m learning F# at the moment and understand both concepts but can\'t seem to combine them.

Suppos

5条回答
  •  天命终不由人
    2020-12-02 15:17

    The predicament of memoizing tail-recursive functions is, of course, that when tail-recursive function

    let f x = 
       ......
       f x1
    

    calls itself, it is not allowed to do anything with a result of the recursive call, including putting it into cache. Tricky; so what can we do?

    The critical insight here is that since the recursive function is not allowed to do anything with a result of recursive call, the result for all arguments to recursive calls will be the same! Therefore if recursion call trace is this

    f x0 -> f x1 -> f x2 -> f x3 -> ... -> f xN -> res
    

    then for all x in x0,x1,...,xN the result of f x will be the same, namely res. So the last invocation of a recursive function, the non-recursive call, knows the results for all the previous values - it is in a position to cache them. The only thing you need to do is to pass a list of visited values to it. Here is what it might look for factorial:

    let cache = Dictionary<_,_>()
    
    let rec fact0 l ((n,res) as arg) = 
        let commitToCache r = 
            l |> List.iter  (fun a -> cache.Add(a,r))
        match cache.TryGetValue(arg) with
        |   true, cachedResult -> commitToCache cachedResult; cachedResult
        |   false, _ ->
                if n = 1 then
                    commitToCache res
                    cache.Add(arg, res)
                    res
                else
                    fact0 (arg::l) (n-1, n*res)
    
    let fact n = fact0 [] (n,1)
    

    But wait! Look - l parameter of fact0 contains all the arguments to recursive calls to fact0 - just like the stack would in a non-tail-recursive version! That is exactly right. Any non-tail recursive algorithm can be converted to a tail-recursive one by moving the "list of stack frames" from stack to heap and converting the "postprocessing" of recursive call result into a walk over that data structure.

    Pragmatic note: The factorial example above illustrates a general technique. It is quite useless as is - for factorial function it is quite enough to cache the top-level fact n result, because calculation of fact n for a particular n only hits a unique series of (n,res) pairs of arguments to fact0 - if (n,1) is not cached yet, then none of the pairs fact0 is going to be called on are.

    Note that in this example, when we went from non-tail-recursive factorial to a tail-recursive factorial, we exploited the fact that multiplication is associative and commutative - tail-recursive factorial execute a different set of multiplications than a non-tail-recursive one.

    In fact, a general technique exists for going from non-tail-recursive to tail-recursive algorithm, which yields an algorithm equivalent to a tee. This technique is called "continuatuion-passing transformation". Going that route, you can take a non-tail-recursive memoizing factorial and get a tail-recursive memoizing factorial by pretty much a mechanical transformation. See Brian's answer for exposition of this method.

提交回复
热议问题