web-dev-qa-db-ja.com

このフィボナッチ関数はどのようにメモされますか?

このフィボナッチ関数はどのようなメカニズムで記憶されていますか?

fib = (map fib' [0..] !!)                 
     where fib' 1 = 1                                                        
           fib' 2 = 1                                                        
           fib' n = fib (n-2) + fib (n-1)                    

また、関連するメモでは、なぜこのバージョンはそうではないのですか?

fib n = (map fib' [0..] !! n)                                               
     where fib' 1 = 1                                                        
           fib' 2 = 1                                                        
           fib' n = fib (n-2) + fib (n-1)                    
112
bjornars

Haskellの評価メカニズムはby-need:値が必要な場合、計算され、再度要求された場合に備えて準備が整っています。リストを定義する場合、_xs=[0..]_の後で100番目の要素_xs!!99_を要求すると、リストの100番目のスロットは「肉付き」になり、番号_99_を保持し、次の準備ができますアクセス。

それが、「リストに行く」というトリックが悪用していることです。通常の二重再帰フィボナッチ定義fib n = fib (n-1) + fib (n-2)では、関数自体が上から2回呼び出され、指数関数的な爆発を引き起こします。しかし、そのトリックを使用して、中間結果のリストを作成し、「リストを調べる」ことにします。

_fib n = (xs!!(n-1)) + (xs!!(n-2)) where xs = 0:1:map fib [2..]
_

秘Theは、そのリストを作成し、そのリストがfibの呼び出しの間に(ガベージコレクションによって)消えないようにすることです。これを達成する最も簡単な方法は、nameそのリストです。 "名前を付けると、それは残ります。"


最初のバージョンでは単相定数を定義し、2番目のバージョンでは多相関数を定義します。ポリモーフィック関数は、提供する必要のある異なるタイプに対して同じ内部リストを使用できないため、no sharing、つまりメモ化はありません。

最初のバージョンでは、コンパイラはgenerousであり、その定数部分式(_map fib' [0..]_)を取り出して、それを別の共有可能なエンティティにしますが、そうする義務はありません。 そして、実際にdo n'tが自動的にそれをしたい場合があります。

edit:)これらの書き換えを検討してください:

_fib1 = f                     fib2 n = f n                 fib3 n = f n          
 where                        where                        where                
  f i = xs !! i                f i = xs !! i                f i = xs !! i       
  xs = map fib' [0..]          xs = map fib' [0..]          xs = map fib' [0..] 
  fib' 1 = 1                   fib' 1 = 1                   fib' 1 = 1          
  fib' 2 = 1                   fib' 2 = 1                   fib' 2 = 1          
  fib' i=fib1(i-2)+fib1(i-1)   fib' i=fib2(i-2)+fib2(i-1)   fib' i=f(i-2)+f(i-1)
_

したがって、実際の話はネストされたスコープ定義についてのようです。 1番目の定義には外部スコープはありません。3番目は、外部スコープ_fib3_ではなく、同じレベルのfを呼び出さないように注意しています。

_fib2_の各新しい呼び出しは、それらのいずれかcould(理論上)異なる定義依存nの値(おかげで)それを指摘してくれたVitusとTikhonに)。最初の定義では依存するnはなく、3番目の定義では依存関係がありますが、_fib3_を呼び出すたびにfが呼び出され、この特定の内部の同じレベルのスコープからのみ定義を呼び出すように注意します_fib3_の呼び出し。したがって、同じxsがその_fib3_の呼び出しに再利用(共有)されます。

しかし、上記のバージョンのいずれかの内部定義が実際にindependentの外部nバインディングであり、 lambdalifting を実行することをコンパイラが認識することを妨げるものはありません。結局、完全なメモ化になります(多態的な定義を除く)。実際、これは、単相型で宣言され、-O2フラグでコンパイルされた場合、3つのバージョンすべてで起こることです。多相型の宣言では、_fib3_はローカル共有を示し、_fib2_はまったく共有しません。

最終的には、コンパイラー、使用されるコンパイラーの最適化、およびテスト方法(GHCIでのファイルのロード、コンパイルの有無、-O2の有無、またはスタンドアロン)、および動作が単相型か多型型かによって異なります完全に変更-ローカル(呼び出しごと)共有(つまり、各呼び出しで線形時間)、メモ化(つまり、最初の呼び出しで線形時間、および同じまたはより小さい引数を持つ後続の呼び出しで0時間)を示すか、まったく共有しないか(指数時間)。

簡単に言えば、それはコンパイラのことです。 :)

93
Will Ness

私は完全に確信しているわけではありませんが、ここでは経験に基づいた推測があります

コンパイラは、fib nは異なるnで異なる可能性があるため、毎回リストを再計算する必要があります。結局、whereステートメントcould内のビットはnに依存します。つまり、この場合、数字のリスト全体は本質的にnの関数です。

バージョンwithoutnは、リストを1回作成し、関数でラップできます。リストcannotは渡されたnの値に依存し、これは簡単に確認できます。リストは定数であり、その後にインデックスが作成されます。もちろん、遅延評価されるのは定数なので、プログラムは(無限の)リスト全体をすぐに取得しようとしません。これは定数であるため、関数呼び出し全体で共有できます。

再帰呼び出しはリスト内の値を検索するだけでよいため、これはまったく記憶されています。 fibバージョンはリストを遅延的に作成するため、冗長な計算を行わずに答えを得るのに十分なだけ計算します。ここで、「遅延」とは、リスト内の各エントリがサンク(評価されていない式)であることを意味します。あなたがdoサンクを評価するとき、それは値になるので、次回アクセスすると計算は繰り返されません。リストは呼び出し間で共有できるため、前のエントリはすべて、次のエントリが必要になるまでにすでに計算されています。

それは本質的に、GHCの怠laなセマンティクスに基づいた賢明で低賃料の動的プログラミングです。標準は、non-strictでなければならないことのみを規定しているため、準拠コンパイラはこのコードをnotメモ。ただし、実際には、すべての妥当なコンパイラは遅延します。

2番目のケースがまったく機能する理由の詳細については、 再帰的に定義されたリスト(zipWithに関するfibs)について を参照してください。

23
Tikhon Jelvis

まず、ghc-7.4.2で、-O2、メモされていないバージョンはそれほど悪くありません。フィボナッチ数の内部リストは、関数の各トップレベル呼び出しに対してまだメモされています。ただし、異なるトップレベルコール間でメモすることはできません。ただし、他のバージョンでは、リストは呼び出し間で共有されます。

これは、単相性の制限によるものです。

1つは単純なパターンバインディング(名前のみ、引数なし)によってバインドされているため、単相性の制限により、単相型を取得する必要があります。推定されるタイプは

fib :: (Num n) => Int -> n

そして、そのような制約はデフォルトで(デフォルト宣言がない場合)Integerにデフォルト設定され、型を次のように修正します。

fib :: Int -> Integer

したがって、リストは1つだけです(タイプ[Integer])メモする。

2番目は関数の引数で定義されているため、多態性のままです。内部リストが呼び出し間でメモされている場合、Numの各タイプに対して1つのリストをメモする必要があります。それは実用的ではありません。

単相性制限を無効にして、または同じ型シグネチャで両方のバージョンをコンパイルし、両方ともまったく同じ動作を示します。 (古いバージョンのコンパイラには当てはまりませんでした。どのバージョンが最初にそれを行ったかはわかりません。)

20
Daniel Fischer

Haskellのメモ機能は必要ありません。経験的なプログラミング言語のみがその機能を必要とします。ただし、Haskelは機能的な言語であり、...

したがって、これは非常に高速なフィボナッチアルゴリズムの例です。

fib = zipWith (+) (0:(1:fib)) (1:fib)

zipWithは標準Preludeの機能です。

zipWith :: (a->b->c) -> [a]->[b]->[c]
zipWith op (n1:val1) (n2:val2) = (n1 + n2) : (zipWith op val1 val2)
zipWith _ _ _ = []

テスト:

print $ take 100 fib

出力:

[1,2,3,5,8,13,21,34,55,89,144,233,377,610,987,1597,2584,4181,6765,10946,17711,28657,46368,75025,121393,196418,317811,514229,832040,1346269,2178309,3524578,5702887,9227465,14930352,24157817,39088169,63245986,102334155,165580141,267914296,433494437,701408733,1134903170,1836311903,2971215073,4807526976,7778742049,12586269025,20365011074,32951280099,53316291173,86267571272,139583862445,225851433717,365435296162,591286729879,956722026041,1548008755920,2504730781961,4052739537881,6557470319842,10610209857723,17167680177565,27777890035288,44945570212853,72723460248141,117669030460994,190392490709135,308061521170129,498454011879264,806515533049393,1304969544928657,2111485077978050,3416454622906707,5527939700884757,8944394323791464,14472334024676221,23416728348467685,37889062373143906,61305790721611591,99194853094755497,160500643816367088,259695496911122585,420196140727489673,679891637638612258,1100087778366101931,1779979416004714189,2880067194370816120,4660046610375530309,7540113804746346429,12200160415121876738,19740274219868223167,31940434634990099905,51680708854858323072,83621143489848422977,135301852344706746049,218922995834555169026,354224848179261915075,573147844013817084101]

経過時間:0.00018秒