このフィボナッチ関数はどのように記憶されていますか?


114

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

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)                    

13
わずかunrelatedly、fib 0終了しません:あなたは、おそらくのための基本例たくfib'なければfib' 0 = 0とをfib' 1 = 1
2012

1
最初のバージョンをより簡潔にすることができることに注意してください:fibs = 1:1:zipWith (+) fibs (tail fibs)fib = (fibs !!)
Bastian 2012

回答:


95

Haskellでは評価メカニズムがあることで、必要な値が必要になったとき、それは計算され、それが再びを求めている場合には準備ができて保持されています。リストを定義し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..]

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


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

最初のバージョンでは、コンパイラーは私たちに寛大になり、その定数部分式(map fib' [0..])を取り除き、それを別個の共有可能エンティティーにしましたが、そうする義務はありません。実際には、そうしないケースもありますそれを自動的に実行したくます。

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

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番目はouter-scopeを呼び出さないように注意していますfib3が、同じレベルfです。

の値に応じて(理論的には)異なる方法で定義できるfib2ため、の新しい呼び出しごとに、ネストされた定義が新たに作成されるようです(VitusとTikhonが指摘してくれたことに感謝します)。最初defintionと全くありませんに依存すること、および第三との依存関係、それだけにはそれぞれ別個のコールがあるへの呼び出しのこの特定の呼び出しに内部同じレベルのスコープからのみ呼び出し定義に慎重である同じように、取得その呼び出しのために再利用(つまり共有)nnfib3ffib3xsfib3

しかし、コンパイラが上記のバージョンの内部定義が実際には外部バインディングから独立していることを認識して、結局ラムダリフティングnを実行し、完全なメモ化を行うことを妨げるものはありません(ポリモーフィック定義を除く)。実際、単相型で宣言され、-O2フラグでコンパイルされたときに、3つのバージョンすべてでまさにそれが起こります。ポリモーフィックな型宣言により、ローカル共有を示し、共有はまったく行いません。fib3fib2

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

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


4
ほんの少しの詳細を修正するには:ローカル関数は、ため、2番目のバージョンは、主に任意の共有を取得していないfib'すべてのために再定義されるnため、fib'fib 1fib'fib 2もリストが異なっている暗示され、。タイプを単相型に修正しても、この振る舞いを示します。
Vitus

1
where句はlet、式と同様の共有を導入しますが、このような問題を隠す傾向があります。もう少し明示的に書き直すと、次のようになります。hpaste.org
Vitus

1
書き換えに関するもう1つの興味深い点:それらに単相型(つまりInt -> Integer)を指定fib2すると、指数時間で実行されfib1fib3両方とも線形時間で実行されますが、fib1メモも付けfib3られますn。これも、ローカル定義がごとに再定義されるためです。
Vitus

1
@misterbeeしかし、確かにコンパイラから何らかの保証があるといいでしょう。特定のエンティティのメモリ常駐に対するある種の制御。共有したいときもあれば、防止したいときもあります。私はそれが可能であるべきだと思います/願っています...
ネス

1
@ElizaBrandtが意味したのは、重いものを再計算して、メモリに保持されないようにすることです。つまり、再計算のコストは、巨大なメモリ保持のコストよりも低くなります。1つの例は、パワーセットの作成です。つまり、独立して2回計算されるpwr (x:xs) = pwr xs ++ map (x:) pwr xs ; pwr [] = [[]]必要pwr xsがあるため、生成および消費されているときにオンザフライでガベージコレクションできます。
Will Ness

23

私は完全には定かではありませんが、これは知識に基づいた推測です:

コンパイラーは、fib n別のものでは異なる可能性があることを想定していますnと可能性があるため、そのたびにリストを再計算する必要ます。結局のところ、whereステートメント内のビットはに依存する可能性がありますn。つまり、この場合、数値のリスト全体は基本的にn

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

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

これは本質的に、GHCの遅延セマンティクスに基づいた動的プログラミングの賢くてローレントな形式です。私は標準がそれが非厳密でなければならないことを指定しているだけだと思うので、準拠コンパイラメモしないようにこのコードをコンパイルする可能性があります。ただし、実際には、すべての妥当なコンパイラーは遅延します。

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


おそらく「fib' n別のものでは異なる可能性がある」という意味nですか?
ネスは

私はあまり明確ではなかったと思います。つまりfib、を含めfib'、内部のすべてが異なる可能性があるということnです。元の例は、他のものを隠すfib'独自のnものにも依存しているため、少し混乱していると思いますn
Tikhon Jelvis

20

まず、でコンパイルされたghc-7.4.2を使用すると-O2、メモ化されていないバージョンはそれほど悪くはありません。フィボナッチ数の内部リストは、関数のトップレベルの呼び出しごとに引き続きメモ化されます。しかし、それは異なるトップレベルの呼び出しでメモすることはできず、合理的に行うこともできません。ただし、他のバージョンでは、リストは呼び出し間で共有されます。

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

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

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

そして、そのような制約はデフォルトになり(デフォルト宣言がなければ)Integer、にタイプを修正します。

fib :: Int -> Integer

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

2つ目は関数の引数で定義されているため、多態性が維持され、内部リストが複数の呼び出しにわたってメモされた場合、1つのリストをの各型についてメモする必要がありますNum。それは現実的ではありません。

モノモーフィズム制限を無効にして、または同じ型シグニチャーを使用して両方のバージョンをコンパイルすると、どちらもまったく同じ動作を示します。(これは古いバージョンのコンパイラには当てはまりませんでした。どのバージョンが最初にそれを行ったかはわかりません。)


タイプごとにリストをメモしておくことが実用的でないのはなぜですか?原則として、GHCは、実行時に遭遇した各Numタイプの部分的に計算されたリストを含む辞書(タイプのクラス制約付き関数を呼び出すのと同じようなもの)を作成できますか?
ミスタービー2014年

1
@misterbee原則として可能ですが、プログラムがfib 1000000多くの型を呼び出すと、大量のメモリを消費します。これを回避するには、キャッシュが大きくなりすぎたときにキャッシュから破棄するヒューリスティックなリストが必要になります。そして、そのようなメモ化戦略はおそらく他の関数または値にも適用されるので、コンパイラーは潜在的に多くのタイプをメモするために潜在的に非常に多くの事柄を扱わなければならないでしょう。合理的な方法でヒューリスティックに(部分的に)ポリモーフィックなメモを実装することは可能だと思いますが、それが価値があるとは思えません。
ダニエルフィッシャー

5

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.00018s


このソリューションは素晴らしいです!
Larry
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.