Combine memoization and tail-recursion

前端 未结 5 1152
再見小時候
再見小時候 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:12

    I'm not sure if there's a simpler way to do this, but one approach would be to create a memoizing y-combinator:

    let memoY f =
      let cache = Dictionary<_,_>()
      let rec fn x =
        match cache.TryGetValue(x) with
        | true,y -> y
        | _ -> let v = f fn x
               cache.Add(x,v)
               v
      fn
    

    Then, you can use this combinator in lieu of "let rec", with the first argument representing the function to call recursively:

    let tailRecFact =
      let factHelper fact (x, res) = 
        printfn "%i,%i" x res
        if x = 0 then res 
        else fact (x-1, x*res)
      let memoized = memoY factHelper
      fun x -> memoized (x,1)
    

    EDIT

    As Mitya pointed out, memoY doesn't preserve the tail recursive properties of the memoee. Here's a revised combinator which uses exceptions and mutable state to memoize any recursive function without overflowing the stack (even if the original function is not itself tail recursive!):

    let memoY f =
      let cache = Dictionary<_,_>()
      fun x ->
        let l = ResizeArray([x])
        while l.Count <> 0 do
          let v = l.[l.Count - 1]
          if cache.ContainsKey(v) then l.RemoveAt(l.Count - 1)
          else
            try
              cache.[v] <- f (fun x -> 
                if cache.ContainsKey(x) then cache.[x] 
                else 
                  l.Add(x)
                  failwith "Need to recurse") v
            with _ -> ()
        cache.[x]
    

    Unfortunately, the machinery which is inserted into each recursive call is somewhat heavy, so performance on un-memoized inputs requiring deep recursion can be a bit slow. However, compared to some other solutions, this has the benefit that it requires fairly minimal changes to the natural expression of recursive functions:

    let fib = memoY (fun fib n -> 
      printfn "%i" n; 
      if n <= 1 then n 
      else (fib (n-1)) + (fib (n-2)))
    
    let _ = fib 5000
    

    EDIT

    I'll expand a bit on how this compares to other solutions. This technique takes advantage of the fact that exceptions provide a side channel: a function of type 'a -> 'b doesn't actually need to return a value of type 'b, but can instead exit via an exception. We wouldn't need to use exceptions if the return type explicitly contained an additional value indicating failure. Of course, we could use the 'b option as the return type of the function for this purpose. This would lead to the following memoizing combinator:

    let memoO f =
      let cache = Dictionary<_,_>()
      fun x ->
        let l = ResizeArray([x])
        while l.Count <> 0 do
          let v = l.[l.Count - 1]
          if cache.ContainsKey v then l.RemoveAt(l.Count - 1)
          else
            match f(fun x -> if cache.ContainsKey x then Some(cache.[x]) else l.Add(x); None) v with
            | Some(r) -> cache.[v] <- r; 
            | None -> ()
        cache.[x]
    

    Previously, our memoization process looked like:

    fun fib n -> 
      printfn "%i" n; 
      if n <= 1 then n 
      else (fib (n-1)) + (fib (n-2))
    |> memoY
    

    Now, we need to incorporate the fact that fib should return an int option instead of an int. Given a suitable workflow for option types, this could be written as follows:

    fun fib n -> option {
      printfn "%i" n
      if n <= 1 then return n
      else
        let! x = fib (n-1)
        let! y = fib (n-2)
        return x + y
    } |> memoO
    

    However, if we're willing to change the return type of the first parameter (from int to int option in this case), we may as well go all the way and just use continuations in the return type instead, as in Brian's solution. Here's a variation on his definitions:

    let memoC f =
      let cache = Dictionary<_,_>()
      let rec fn n k =
        match cache.TryGetValue(n) with
        | true, r -> k r
        | _ -> 
            f fn n (fun r ->
              cache.Add(n,r)
              k r)
      fun n -> fn n id
    

    And again, if we have a suitable computation expression for building CPS functions, we can define our recursive function like this:

    fun fib n -> cps {
      printfn "%i" n
      if n <= 1 then return n
      else
        let! x = fib (n-1)
        let! y = fib (n-2)
        return x + y
    } |> memoC
    

    This is exactly the same as what Brian has done, but I find the syntax here is easier to follow. To make this work, all we need are the following two definitions:

    type CpsBuilder() =
      member this.Return x k = k x
      member this.Bind(m,f) k = m (fun a -> f a k)
    
    let cps = CpsBuilder()
    

提交回复
热议问题