代码之家  ›  专栏  ›  技术社区  ›  Ronald Wildenberg

结合记忆和尾部递归

  •  30
  • Ronald Wildenberg  · 技术社区  · 14 年前

    有没有可能把记忆化和尾部递归结合起来呢?目前我正在学习F#,理解这两个概念,但似乎无法将它们结合起来。

    假设我有以下内容 memoize 函数(从 Real-World Functional Programming ):

    let memoize f = let cache = new Dictionary<_, _>()
                    (fun x -> match cache.TryGetValue(x) with
                              | true, y -> y
                              | _       -> let v = f(x)
                                           cache.Add(x, v)
                                           v)
    

    以下是 factorial

    let rec factorial(x) = if (x = 0) then 1 else x * factorial(x - 1)
    

    阶乘 也不是很难做到:

    let rec memoizedFactorial =
      memoize (fun x -> if (x = 0) then 1 else x * memoizedFactorial(x - 1))
    
    let tailRecursiveFactorial(x) =
      let rec factorialUtil(x, res) = if (x = 0)
                                      then res
                                      else let newRes = x * res
                                           factorialUtil(x - 1, newRes)
      factorialUtil(x, 1)
    

    但是你能把记忆和尾部递归结合起来吗?我做了一些尝试,但似乎没有成功。或者这根本不可能?

    5 回复  |  直到 14 年前
        1
  •  23
  •   Brian    14 年前

    一如既往,延续产生了一个优雅的追尾解决方案:

    open System.Collections.Generic 
    
    let cache = Dictionary<_,_>()  // TODO move inside 
    let memoizedTRFactorial =
        let rec fac n k =  // must make tailcalls to k
            match cache.TryGetValue(n) with
            | true, r -> k r
            | _ -> 
                if n=0 then
                    k 1
                else
                    fac (n-1) (fun r1 ->
                        printfn "multiplying by %d" n  //***
                        let r = r1 * n
                        cache.Add(n,r)
                        k r)
        fun n -> fac n id
    
    printfn "---"
    let r = memoizedTRFactorial 4
    printfn "%d" r
    for KeyValue(k,v) in cache do
        printfn "%d: %d" k v
    
    printfn "---"
    let r2 = memoizedTRFactorial 5
    printfn "%d" r2
    
    printfn "---"
    
    // comment out *** line, then run this
    //let r3 = memoizedTRFactorial 100000
    //printfn "%d" r3
    

    有两种测试。首先,这个演示调用F(4)可以根据需要缓存F(4)、F(3)、F(2)、F(1)。

    然后,注释掉 ***

    下一步,我想,把记忆分解和阶乘分离开来:

    open System.Collections.Generic 
    
    let cache = Dictionary<_,_>()  // TODO move inside 
    let memoize fGuts n =
        let rec newFunc n k =  // must make tailcalls to k
            match cache.TryGetValue(n) with
            | true, r -> k r
            | _ -> 
                fGuts n (fun r ->
                            cache.Add(n,r)
                            k r) newFunc
        newFunc n id 
    let TRFactorialGuts n k memoGuts =
        if n=0 then
            k 1
        else
            memoGuts (n-1) (fun r1 ->
                printfn "multiplying by %d" n  //***
                let r = r1 * n
                k r) 
    
    let memoizedTRFactorial = memoize TRFactorialGuts 
    
    printfn "---"
    let r = memoizedTRFactorial 4
    printfn "%d" r
    for KeyValue(k,v) in cache do
        printfn "%d: %d" k v
    
    printfn "---"
    let r2 = memoizedTRFactorial 5
    printfn "%d" r2
    
    printfn "---"
    
    // comment out *** line, then run this
    //let r3 = memoizedTRFactorial 100000
    //printfn "%d" r3
    

    编辑

    open System.Collections.Generic 
    
    let memoize fGuts =
        let cache = Dictionary<_,_>()
        let rec newFunc n k =  // must make tailcalls to k
            match cache.TryGetValue(n) with
            | true, r -> k r
            | _ -> 
                fGuts n (fun r ->
                            cache.Add(n,r)
                            k r) newFunc
        cache, (fun n -> newFunc n id)
    let TRFactorialGuts n k memoGuts =
        if n=0 then
            k 1
        else
            memoGuts (n-1) (fun r1 ->
                printfn "multiplying by %d" n  //***
                let r = r1 * n
                k r) 
    
    let facCache,memoizedTRFactorial = memoize TRFactorialGuts 
    
    printfn "---"
    let r = memoizedTRFactorial 4
    printfn "%d" r
    for KeyValue(k,v) in facCache do
        printfn "%d: %d" k v
    
    printfn "---"
    let r2 = memoizedTRFactorial 5
    printfn "%d" r2
    
    printfn "---"
    
    // comment out *** line, then run this
    //let r3 = memoizedTRFactorial 100000
    //printfn "%d" r3
    
    let TRFibGuts n k memoGuts =
        if n=0 || n=1 then
            k 1
        else
            memoGuts (n-1) (fun r1 ->
                memoGuts (n-2) (fun r2 ->
                    printfn "adding %d+%d" r1 r2 //%%%
                    let r = r1+r2
                    k r)) 
    let fibCache, memoizedTRFib = memoize TRFibGuts 
    printfn "---"
    let r5 = memoizedTRFib 4
    printfn "%d" r5
    for KeyValue(k,v) in fibCache do
        printfn "%d: %d" k v
    
    printfn "---"
    let r6 = memoizedTRFib 5
    printfn "%d" r6
    
    printfn "---"
    
    // comment out %%% line, then run this
    //let r7 = memoizedTRFib 100000
    //printfn "%d" r7
    
        2
  •  15
  •   Dmitry Lomov    14 年前

    记忆尾部递归函数的困境当然是,当尾部递归函数

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

    调用本身时,不允许对递归调用的结果执行任何操作,包括将其放入缓存。狡猾的;那我们该怎么办?

    这里的关键是,由于递归函数不允许对递归调用的结果执行任何操作,因此递归调用的所有参数的结果都是相同的!因此,如果递归调用跟踪是这样的

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

    f x 所以递归函数的最后一次调用,非递归调用,知道前面所有值的结果-它可以缓存它们。您唯一需要做的就是将访问的值的列表传递给它。以下是它可能会寻找阶乘:

    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)
    

    l 的参数 fact0 包含递归调用的所有参数 事实0 -就像非尾部递归版本中的堆栈一样!这是完全正确的。任何非尾部递归算法都可以通过将“堆栈帧列表”从一个堆栈移动到另一个堆,并将递归调用结果的“后处理”转换为遍历该数据结构的方式来转换为尾部递归算法。

    实用注释:上面的阶乘示例说明了一种通用技术。它是非常无用的,因为它是非常充分的阶乘函数缓存的顶级 fact n 结果,因为 对于一个特定的n只命中fact0的一系列唯一的(n,res)参数对-如果(n,1)还没有被缓存,那么fact0的所有参数对都不会被调用。

    注意,在这个例子中,当我们从非尾部递归阶乘到尾部递归阶乘时,我们利用了乘法是结合的和交换的这一事实-尾部递归阶乘执行一组不同于非尾部递归阶乘的乘法。

    事实上,存在一种从非尾部递归到尾部递归算法的通用技术,它产生一个等价于tee的算法。这种技术被称为“连续传递变换”。沿着这条路线,你可以使用一个非尾部递归记忆阶乘,通过一个机械变换得到一个尾部递归记忆阶乘。关于这个方法的解释,请看布莱恩的答案。

        3
  •  8
  •   kvb    14 年前

    我不确定是否有更简单的方法,但一种方法是创建一个记忆y组合器:

    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
    

    然后,您可以使用此组合符代替“let rec”,第一个参数表示要递归调用的函数:

    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)
    

    编辑

    正如米提亚指出的, memoY

    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]
    

    不幸的是,插入到每个递归调用中的机器有点繁重,因此需要深度递归的未记忆输入的性能可能会有点慢。但是,与其他一些解决方案相比,它的优点是对递归函数的自然表达式的更改非常小:

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

    编辑

    'a -> 'b 实际上不需要返回类型为的值 'b 'b option

    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]
    

    以前,我们的回忆录过程是这样的:

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

    现在,我们需要考虑一个事实 fib int option 而不是 int . 提供一个合适的工作流 option 类型,可以编写如下:

    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
    

    但是,如果我们愿意更改第一个参数的返回类型(从 内景 int选项 在本例中,我们不妨一直这样做,只在返回类型中使用continuations,就像Brian的解决方案一样。他的定义有一个变化:

    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
    

    同样,如果我们有一个合适的计算表达式来构建CPS函数,我们可以这样定义递归函数:

    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
    

    这与Brian所做的完全相同,但我发现这里的语法更容易理解。为了使这项工作顺利进行,我们只需要以下两个定义:

    type CpsBuilder() =
      member this.Return x k = k x
      member this.Bind(m,f) k = m (fun a -> f a k)
    
    let cps = CpsBuilder()
    
        4
  •  3
  •   gradbot    14 年前

    ......720 // factorial 6
    ......720 // factorial 6
    .....120  // factorial 5
    
    ......720 // memoizedFactorial 6
    720       // memoizedFactorial 6
    120       // memoizedFactorial 5
    
    ......720 // tailRecFact 6
    720       // tailRecFact 6
    .....120  // tailRecFact 5
    
    ......720 // tailRecursiveMemoizedFactorial 6
    720       // tailRecursiveMemoizedFactorial 6
    .....120  // tailRecursiveMemoizedFactorial 5
    

    kvb的解决方案返回相同的结果,就像这个函数一样直接记忆。

    let tailRecursiveMemoizedFactorial = 
        memoize 
            (fun x ->
                let rec factorialUtil x res = 
                    if x = 0 then 
                        res
                    else 
                        printf "." 
                        let newRes = x * res
                        factorialUtil (x - 1) newRes
    
                factorialUtil x 1
            )
    

    测试源代码。

    open System.Collections.Generic
    
    let memoize f = 
        let cache = new Dictionary<_, _>()
        (fun x -> 
            match cache.TryGetValue(x) with
            | true, y -> y
            | _ -> 
                let v = f(x)
                cache.Add(x, v)
                v)
    
    let rec factorial(x) = 
        if (x = 0) then 
            1 
        else
            printf "." 
            x * factorial(x - 1)
    
    let rec memoizedFactorial =
        memoize (
            fun x -> 
                if (x = 0) then 
                    1 
                else 
                    printf "."
                    x * memoizedFactorial(x - 1))
    
    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
    
    let tailRecFact =
      let factHelper fact (x, res) = 
        if x = 0 then 
            res 
        else
            printf "." 
            fact (x-1, x*res)
      let memoized = memoY factHelper
      fun x -> memoized (x,1)
    
    let tailRecursiveMemoizedFactorial = 
        memoize 
            (fun x ->
                let rec factorialUtil x res = 
                    if x = 0 then 
                        res
                    else 
                        printf "." 
                        let newRes = x * res
                        factorialUtil (x - 1) newRes
    
                factorialUtil x 1
            )
    
    factorial 6 |> printfn "%A"
    factorial 6 |> printfn "%A"
    factorial 5 |> printfn "%A\n"
    
    memoizedFactorial 6 |> printfn "%A"
    memoizedFactorial 6 |> printfn "%A"
    memoizedFactorial 5 |> printfn "%A\n"
    
    tailRecFact 6 |> printfn "%A"
    tailRecFact 6 |> printfn "%A"
    tailRecFact 5 |> printfn "%A\n"
    
    tailRecursiveMemoizedFactorial 6 |> printfn "%A"
    tailRecursiveMemoizedFactorial 6 |> printfn "%A"
    tailRecursiveMemoizedFactorial 5 |> printfn "%A\n"
    
    System.Console.ReadLine() |> ignore
    
        5
  •  0
  •   nicolas    12 年前

    let rec y f x = f (y f) x
    
    let memoize (d:System.Collections.Generic.Dictionary<_,_>) f n = 
       if d.ContainsKey n then d.[n] 
       else d.Add(n, f n);d.[n]
    
    let rec factorialucps factorial' n cont = 
        if n = 0I then cont(1I) else factorial' (n-1I) (fun k -> cont (n*k))
    
    let factorialdpcps  = 
        let d =  System.Collections.Generic.Dictionary<_, _>()
        fun n ->  y (factorialucps >> fun f n -> memoize d f n ) n id
    
    
    factorialdpcps 15I //1307674368000