31

どうにかしてメモ化と末尾再帰を組み合わせることができますか? 私は現在 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)

メモ化factorialはそれほど難しくなく、末尾再帰にすることも難しくありません。

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)

しかし、メモ化と末尾再帰を組み合わせることができますか? 私はいくつかの試みをしましたが、うまくいかないようです。それとも、これは単に不可能ですか?

4

5 に答える 5

23

いつものように、継続は洗練されたテールコール ソリューションをもたらします。

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

テストには 2 種類あります。まず、このデモでは、F(4) を呼び出すと、F(4)、F(3)、F(2)、F(1) が必要に応じてキャッシュされます。

次に、printf をコメントアウトし***、最終テストのコメントを外して (そしてリリース モードでコンパイルして)、StackOverflow ではないことを示します (末尾呼び出しを正しく使用します)。

おそらく、'memoize' を一般化して、次に 'fib' でデモンストレーションします...

編集

OK、これが次のステップだと思います。メモ化を階乗から分離します。

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
于 2010-08-11T19:10:37.007 に答える
15

末尾再帰関数をメモ化する際の苦境は、もちろん、末尾再帰関数が

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

再帰呼び出しの結果に対して、それをキャッシュに入れることを含め、何もすることはできません。トリッキー; では、何ができるでしょうか?

ここでの重要な洞察は、再帰関数は再帰呼び出しの結果に対して何もできないため、再帰呼び出しのすべての引数の結果は同じになるということです! したがって、再帰呼び出しトレースがこれである場合

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

その場合、x0、x1、...、xN のすべての x の結果はf x同じ、つまり res になります。したがって、再帰関数の最後の呼び出しである非再帰呼び出しは、以前のすべての値の結果を知っています。それらをキャッシュする立場にあります。行う必要があるのは、訪問した値のリストを渡すことだけです。階乗の場合は次のようになります。

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は、再帰呼び出しへのすべての引数が含まれていますfact0- スタックが非末尾再帰バージョンでそうするのと同じように! その通りです。非末尾再帰アルゴリズムは、「スタック フレームのリスト」をスタックからヒープに移動し、再帰呼び出し結果の「後処理」をそのデータ構造のウォークに変換することにより、末尾再帰アルゴリズムに変換できます。

実用的な注意: 上記の階乗の例は、一般的な手法を示しています。そのままではまったく役に立たない - 階乗関数の場合、トップレベルのfact n結果をキャッシュするだけで十分fact nです. 1) まだキャッシュされていない場合、fact0 が呼び出されるペアはありません。

この例では、非末尾再帰階乗から末尾再帰階乗に移行したときに、乗算が結合的かつ交換可能であるという事実を利用したことに注意してください。末尾再帰階乗は、非末尾再帰とは異なる一連の乗算を実行します。再帰的なもの。

実際、非末尾再帰アルゴリズムから末尾再帰アルゴリズムに移行するための一般的な手法が存在し、ティーと同等のアルゴリズムが得られます。この手法は「継続通過変換」と呼ばれます。そのルートをたどると、ほとんど機械的な変換によって、非末尾再帰メモ化階乗を取得し、末尾再帰メモ化階乗を取得できます。この方法の説明については、ブライアンの回答を参照してください。

于 2010-08-11T17:47:22.257 に答える
8

これを行うためのより簡単な方法があるかどうかはわかりませんが、1 つのアプローチは、メモ化する 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)

編集

Mitya が指摘したように、メモ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

編集

これが他のソリューションとどのように比較されるかについて少し詳しく説明します。この手法は、例外がサイド チャネルを提供するという事実を利用しています。 type の関数は、'a -> 'b実際には type の値を返す必要はありませんが'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 optionintoption

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

ただし、最初のパラメーターの戻り値の型を変更したい場合 (この場合はinttoからint option)、ブライアンのソリューションのように、代わりに戻り値の型で継続を使用することもできます。彼の定義のバリエーションは次のとおりです。

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 が行ったこととまったく同じですが、ここの構文の方が理解しやすいと思います。これを機能させるために必要なのは、次の 2 つの定義だけです。

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

let cps = CpsBuilder()
于 2010-08-11T15:12:24.390 に答える
3

メモ化を視覚化するテストを書きました。各ドットは再帰呼び出しです。

......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
于 2010-08-11T16:59:57.423 に答える
0

y を介した相互末尾再帰がスタック フレームを作成していない場合、これは機能するはずです。

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
于 2012-12-26T20:01:15.227 に答える