Haskellのメモ化の背後にある概念を誰かが説明できますか?


12

(コーディングの問題ではなく、概念的なメカニズムに関するものなので、ここに質問を入れています)

私はそのequasionでフィボナッチ数列を使用していた小さなプログラム、に取り組んでいたが、私は特定の数を乗り越えた場合、それは痛々しいほど遅くなったことに気づいた、私はとして知られているハスケルの技術でつまずい少し周りグーグルMemoization、彼らは次のように動作するコードを示しました:

-- Traditional implementation of fibonacci, hangs after about 30
slow_fib :: Int -> Integer
slow_fib 0 = 0
slow_fib 1 = 1
slow_fib n = slow_fib (n-2) + slow_fib (n-1)

-- Memorized variant is near instant even after 10000
memoized_fib :: Int -> Integer
memoized_fib = (map fib [0 ..] !!)
   where fib 0 = 0
         fib 1 = 1
         fib n = memoized_fib (n-2) + memoized_fib (n-1)

皆さんへの私の質問は、これがどのようにまたはなぜ機能するのかということです。

それは、計算が追いつく前に、なんとかしてリストの大部分を処理することができたからでしょうか?しかし、haskellが遅延している場合、追いつく必要のある計算は実際にはありません...では、どのように動作しますか?


1
どういう意味the calculation catches upですか?ところで、メモ化はhaskellに固有ではありません:en.wikipedia.org/wiki/
サイモン

Killanの答えの下で私の説明を参照してください
エレクトリックコーヒー

2
あなたの質問が大好きです。ちょうど簡単なメモ:技術がメモと呼ばれる私はないメモ、zation zation。
ラチェット

回答:


11

実際のメモ化の背後にあるメカニズムを説明するために、

memo_fib = (map fib [1..] !!)

「サンク」、未評価の計算のリストを生成します。これらを未開封のプレゼントと考えてください。触れない限り、それらは実行されません。

サンクを評価したら、それを再度評価することはありません。これは実際には「通常の」ハスケルにおける唯一の突然変異の形式であり、サンクは一度評価されて具体的な値になると突然変異します。

コードに戻ると、サンクのリストがあり、このツリー再帰を実行しますが、リストを使用して再帰し、リスト内の要素が評価されると、再び計算されることはありません。したがって、単純なfib関数でのツリーの再帰を回避します。

接線上興味深いことに、このリストは一度だけ評価されるため、一連のフィボナッチ数が計算される場合、これは特に高速です。つまりmemo_fib 10000、2回計算する場合、2回目は瞬時に計算されます。これは、Haskellが関数の引数を1回しか評価せず、ラムダではなく部分的なアプリケーションを使用しているためです。

TLDR:計算をリストに保存することにより、リストの各要素が1回評価されるため、各fibonnacci番号はプログラム全体で正確に1回計算されます。

可視化:

 [THUNK_1, THUNK_2, THUNK_3, THUNK_4, THUNK_5]
 -- Evaluating THUNK_5
 [THUNK_1, THUNK_2, THUNK_3, THUNK_4, THUNK_3 + THUNK_4]
 [THUNK_1, THUNK_2, THUNK_1 + THUNK_2, THUNK_4, THUNK_3 + THUNK_4]
 [1, 1, 1 + 1, THUNK_4, THUNK_3 + THUNK_4]
 [1, 1, 2, THUNK_4, 2 + THUNK4]
 [1, 1, 2, 1 + 2, 2 + THUNK_4]
 [1, 1, 2, 3, 2 + 3]
 [1, 1, 2, 3, 5]

そのTHUNK_4ため、サブ式はすでに評価されているため、評価方法がはるかに高速であることがわかります。


リスト内の値が短いシーケンスでどのように動作するかの例を提供できますか?私はそれがどのように動作するかの視覚化に追加するかもしれないと思います...そしてmemo_fib同じ値で2回呼び出すと、2回目はすぐになりますが、値1でそれを呼び出すと、評価するのにまだ時間がかかります(たとえば30から31に移動するなど)
Electric Coffee

@ElectricCoffeeが追加されました
ダニエル・

@ElectricCoffeeませんが、それ以来ではないだろうmemo_fib 29memo_fib 30、既に評価され、それが正確であれば、これらの2つの数値を追加するのにかかるようになります:)何かがevaledされたら、それはevaledまま。
ダニエル・グラッツァー

1
そうでなければ、どんなパフォーマンスを得ていない、あなたの再帰リストを通過する必要がある@ElectricCoffee
ダニエルGratzer

2
@ElectricCoffeeはい。しかし、リストの31番目の要素は過去の計算を使用していないため、yesをメモしていますが、かなり役に立たない方法です。繰り返される計算は2回計算されませんが、新しい値ごとにツリー再帰があります非常に、非常に遅い
ダニエルグラッツァー

1

メモ化のポイントは、同じ関数を2回計算することではありません。これは、純粋に機能する、つまり副作用のない計算を高速化するのに非常に役立ちます。これはfibo、のような関数の場合に特に必要です。これは、単純に実装された場合、ツリー再帰、つまり指数関数的な努力につながります。(これは、フィボナッチ数が再帰を教える上で実際に非常に悪い例である理由の1つです-チュートリアルや本で見つけるほとんどすべてのデモ実装は、大きな入力値には使用できません。)

実行のフローをトレースすると、2番目のケースでは、実行時にの値fib xが常に使用可能fib x+1あり、ランタイムシステムは別の再帰呼び出しではなくメモリから単純に読み取ることができますが、最初のソリューションは、小さい値の結果が利用可能になる前に、大きいソリューションを計算しようとします。これは最終的に、反復子[0..n]が左から右に評価され、それで始まるため0、最初の例の再帰は次で始まり、nその後だけ尋ねられるためですn-1。これは、多くの不必要な重複関数呼び出しにつながるものです。


ああ、私はそれのポイントを理解し、私はちょうど私がコードで見ることができるものからのように、あなたが書いたときにということで、それがどのように動作するか理解していないmemorized_fib 20例えば、あなたが実際ばかり書いているmap fib [0..] !! 20、それはまだ計算する必要があるだろう20までの数字の全範囲、またはここに何かが欠けていますか?
エレクトリックコーヒー

1
はい、ただし各番号につき1回だけです。素朴な実装はfib 2頻繁に計算するので、頭を回転させます-呼び出しツリーのファーにのような小さな値を書き留めますn==5。それがあなたを救うものを見たら、あなたは再びメモ化を決して忘れません。
キリアンフォス

@ElectricCoffee:はい、1から20のfibを計算します。その呼び出しからは何も得られません。fib 21を計算してみます。1〜21を計算する代わりに、すでに1〜20が計算されており、再度実行する必要がないため、21を計算することができます。
Phoshi

のコールツリーを書き留めようとしていますがn = 5、現在のところn == 3、これまでのところこれまでのところ良いところに到達しましたが、これは単にこれを考えている私の命令的な心ですがn == 3、それは単に、map fib [0..]!!3?それfib nはプログラムのブランチに入ります...事前に計算されたデータのメリットを正確に得ることができる場所はどこですか?
電気コーヒー

1
いいえ、memoized_fib結構です。それはslow_fibあなたがそれをたどると泣くでしょう。
キリアンフォス
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.