2013-01-30 15 views
8

Haskellを学ぼうとして、関数と再帰を正しく理解するためにpi計算を実装しました。パイを計算するためLeibniz Formulaを使用して数字を表示しようとするとGHCIでスタックオーバーフローが発生する

、私は与えられたパラメータの許容範囲にパイを印刷し、再帰関数の数は、その値を取得するために呼び出す、次を思い付いた:

reverseSign :: (Fractional a, Ord a) => a -> a 
reverseSign num = ((if num > 0 
         then -1 
         else 1) * (abs(num) + 2)) 

piCalc :: (Fractional a, Integral b, Ord a) => a -> (a, b) 
piCalc tolerance = piCalc' 1 0.0 tolerance 0 

piCalc' :: (Ord a, Fractional a, Integral b) => a -> a -> a -> b -> (a, b) 
piCalc' denom prevPi tolerance count = if abs(newPi - prevPi) < tolerance 
             then (newPi, count) 
             else piCalc' (reverseSign denom) newPi tolerance (count + 1) 
             where newPi = prevPi + (4/denom) 

だから私はGHCiの中でこれを実行すると、期待どおりに動作するようです:

*Main> piCalc 0.001 
(3.1420924036835256,2000) 

をしかし、私は私の許容範囲があまりにも細かい設定されている場合、この処理が行われます。

*Main> piCalc 0.0000001 
(3.1415927035898146,*** Exception: stack overflow 

これは私には完全に直感的なようです。実際の計算は正常に動作しますが、再帰呼び出しが何回失敗するかを印刷しようとしていますか?

これはなぜですか?

+3

場合、あなたは(私はHaskellのを始めたとき、私はしませんでした!)サンクがあるかわからない、それは基本的に未解決の計算です。あなたの最初の例では、 'count'を印刷する前に' 2000'の値を持たず、 '... + 1)+1の値を持ちます+1)+1)+1)' ' (私は開始時に2000の左括弧を省きました:P)。それを印刷すると、実際には追加されます。 –

+2

@DanielBuckmasterは、Thunkが構築し続けているメモリの重要なポイントは、メモリがますます増えていく一方で、「カウント」は「Int」(空間内の定数) 。あなたはこれに慣れますが、間違いなくあなたを噛むことができます。 – gspr

答えて

8

これは、従来のfoldl (+) 0 [1..1000000]スタックオーバーフローの変種です。問題は、カウント値がpiCalc'の評価中に決して評価されないということです。これは、必要に応じて追加することを表す塊の数が増え続けることを意味します。必要になると、それを評価するには、サンクの数に比例したスタックの深さが必要であるという事実により、オーバーフローが発生します。

最も簡単な解決策は、これは、それが成長しないことを意味し、パターンが一致したときに評価されるcountの値を強制的に

piCalc' denom prevPi tolerance !count = ... 

piCalc'の開始を変え、BangPatterns拡張子を使用していますサンクの巨大な鎖。等価的に

、および拡張を使用せずに、あなたが

piCalc' denom prevPi tolerance count = count `seq` ... 

としてそれを書くことができます。これは、上記の溶液と意味的にまったく同じですが、それは言語拡張を経て、暗黙的にseq明示的に代わりに使用しています。これにより、移植性は向上しますが、もう少し詳しく説明します。 piCalc'newPiの値を必要とする計算結果の枝、prevPi、およびtolerance:パイの近似は、ネストされたサンクの長いシーケンスではなく、カウントがある理由については

。それが完了したかどうか、または別の反復を実行する必要があるかどうかを判断する前に、それらの値を調べる必要があります。これは、評価を実行させるブランチです(関数アプリケーションが実行されたとき、通常、関数の結果に対して何かがパターンマッチングすることを意味します)。一方、piCalc'の値は、 countであるため、計算中は評価されません。

+1

この例では、thunkingがpiの計算値に起こらない理由を説明できますか? –

+2

@DanielBuckmasterこれは、 'piCalc 'が' newPi'、 'prevPi'、および' tolerance'の値を必要とする計算結果に分岐するためです。それが完了したかどうか、または別の反復を実行する必要があるかどうかを判断する前に、それらの値を調べる必要があります。それは、評価を実行させるブランチです(関数アプリケーションが実行されたとき、通常、関数の結果にパターンマッチングがあることを意味します)。 – Carl

+1

ありがとう!私はそれが答えにあることは非常に貴重だと思います。これは、 'count'が実際の計算ではなくスタックオーバーフローを引き起こす理由です。 –

10

カウントは計算中に評価されることはありませんので、最後まで巨大なサンク(スタックオーバーフロー)として残されます。

あなたは、なぜ我々だけcountの評価を強制する必要がありますBangPatterns拡張を可能にし、piCalc' denom prevPi tolerance !count = ...

を書き込むことによって、計算中にその評価を強制することができますか?他の引数はすべてifで評価されています。 piCalc'を再度呼び出す前に、実際にそれらをすべて検査する必要があるため、サンクは増えません。 「計算可能であることを約束する」だけでなく、実際の値が必要です。一方、countは計算中に決して必要とされず、最後まで一連のサンクとして残ることができます。

関連する問題