無限リストでのfoldlとfoldrの動作


124

この質問の myAny関数のコードでは、foldrを使用しています。述語が満たされると、無限リストの処理を停止します。

私はfoldlを使って書き直しました:

myAny :: (a -> Bool) -> [a] -> Bool
myAny p list = foldl step False list
   where
      step acc item = p item || acc

(step関数への引数は正しく逆になっていることに注意してください。)

ただし、無限リストの処理を停止しなくなりました。

私はApocalispの答えのように関数の実行を追跡しようとしました:

myAny even [1..]
foldl step False [1..]
step (foldl step False [2..]) 1
even 1 || (foldl step False [2..])
False  || (foldl step False [2..])
foldl step False [2..]
step (foldl step False [3..]) 2
even 2 || (foldl step False [3..])
True   || (foldl step False [3..])
True

ただし、これは関数の動作方法ではありません。これはどのように間違っていますか?

回答:


231

どのようにfoldsが異なることは混乱の頻繁な源であると思われるので、ここではより一般的な概要は次のとおりです。

[x1, x2, x3, x4 ... xn ]関数fとシードを使用して、n個の値のリストを折りたたむことを検討してくださいz

foldl です:

  • 左結合f ( ... (f (f (f (f z x1) x2) x3) x4) ...) xn
  • 末尾再帰:リストを反復処理し、後で値を生成します
  • Lazy:結果が必要になるまで何も評価されません
  • 逆方向foldl (flip (:)) []リストを逆にします。

foldr です:

  • 右連想f x1 (f x2 (f x3 (f x4 ... (f xn z) ... )))
  • 引数への再帰:各反復はf、次の値と、リストの残りを折りたたんだ結果に適用されます。
  • Lazy:結果が必要になるまで何も評価されません
  • Forwardsfoldr (:) []リストを変更せずに返します。

ここには、時々人々をつまずかせる少し微妙なポイントがあります。なぜなら、foldl逆であるので、の各アプリケーションは結果の外側f追加されます。そしてlazyなので、結果が必要になるまで何も評価されません。つまり、結果の一部を計算するために、Haskellは最初にリスト全体を反復処理して、ネストされた関数アプリケーションの式を作成し、次に最も外側の関数を評価し、必要に応じてその引数を評価します。常に最初の引数を使用する場合、これはHaskellが最も内側の項まで再帰し、次にの各アプリケーションを逆方向に計算する必要があることを意味します。ff

これは明らかに、ほとんどの関数型プログラマーが知っており、愛している効率的な末尾再帰とはかけ離れています。

実際、foldl技術的に末尾再帰ですが、結果式全体が何かを評価する前に構築されるfoldlため、スタックオーバーフローが発生する可能性があります。

一方、考慮してくださいfoldr。また、怠け者だが、それが実行されるため、前方に、それぞれのアプリケーションがfに追加された内部結果の。したがって、結果を計算するために、Haskellは単一の関数アプリケーションを構築します。その2番目の引数は、折りたたまれたリストの残りの部分です。fがその2番目の引数(データコンストラクターなど)で遅延している場合、結果は段階的に遅延され、フォールドの各ステップは、それを必要とする結果の一部が評価されたときにのみ計算されます。

そうしないと、foldrときどき無限リストが機能する理由がわかりますfoldl。前者は無限リストを別の遅延無限データ構造に遅延変換できますが、後者はリスト全体を検査して結果の一部を生成する必要があります。一方、のfoldrように両方の引数をすぐに必要とする関数では、のよう(+)に機能する(または機能しない)のでfoldl、評価する前に巨大な式を作成します。

したがって、注意すべき2つの重要な点は次のとおりです。

  • foldr ある遅延再帰データ構造を別の構造に変換できます。
  • そうしないと、大規模または無限のリストでスタックオーバーフローが発生して、レイジーフォールドがクラッシュします。

あなたはそれfoldrがすべてfoldlができることに加えてもっとできるように聞こえることに気づいたかもしれません。これは本当です!実際、foldlはほとんど役に立ちません!

しかし、大きな(ただし、無限ではない)リストを折りたたむことにより、遅延のない結果を生成したい場合はどうでしょうか。このために、我々はしたい厳格倍標準ライブラリはthoughfully提供を

foldl' です:

  • 左結合f ( ... (f (f (f (f z x1) x2) x3) x4) ...) xn
  • 末尾再帰:リストを反復処理し、後で値を生成します
  • Strict:各関数アプリケーションは途中で評価されます
  • 逆方向foldl' (flip (:)) []リストを逆にします。

のでfoldl'あり、厳密な、Haskellはなり結果を計算するために評価する f代わりに、左の引数をさせるのは、各ステップでは、巨大な、未評価の表現を蓄積します。これにより、通常の効率的な末尾再帰が実現します。言い換えると:

  • foldl' 大きなリストを効率的に折りたたむことができます。
  • foldl' 無限リストで無限ループ(スタックオーバーフローを引き起こさない)でハングします。

Haskell wikiには、これについて説明するページもあります。


6
なぜ私は好奇心旺盛ですので、私はここに来たfoldrよりも優れているfoldlではHaskellの反対がで真の間、アーラン(私は前に学んだハスケル)。以来、アーランは怠惰ではなく、機能がされていないカレーので、foldlErlangのような挙動foldl'上。これは素晴らしい答えです!お疲れ様でした!
Siu Ching Pong -Asuka Kenji-

7
これはたいてい素晴らしい説明ですが、foldl「後方」およびfoldr「前方」としての記述には問題があると思います。これは、折りが逆方向である理由の図でflip適用さ(:)れているためです。自然な反応は、「もちろん逆です。flipリストの連結を実行しました!」完全な評価では、最初(最も内側)の最初のリスト要素にfoldl適用されるfため、「後方」と呼ばれるのを見るのも奇妙です。それfoldrが「逆方向に実行」さfれ、最後の要素に最初に適用されます。
Dave Abrahams、2014年

1
@DaveAbrahams:ちょうど間foldlfoldrし、厳しさと最適化、第一の手段「最も外側」を無視し、ない「最も内側」。これが、foldr無限リストを処理でき、処理できない理由です。最初のリスト要素とテールを折りたたんだ(評価されていない)結果には、最初のfoldl右折fりが適用されますf
CAマッキャン2014年

1
foldlがfoldlよりも優先されるインスタンスがあるかどうか疑問に思っています。あると思いますか?
kazuoua

1
怠惰が不可欠である@kazuoua、例えばlast xs = foldl (\a z-> z) undefined xs
ネスは

28
myAny even [1..]
foldl step False [1..]
foldl step (step False 1) [2..]
foldl step (step (step False 1) 2) [3..]
foldl step (step (step (step False 1) 2) 3) [4..]

直感的にfoldlは、常に「外側」または「左側」にあるため、最初に展開されます。無限の広告。


10

ここでHaskellのドキュメントを見ると、foldlは末尾再帰であり、値を返す前に次のパラメータで呼び出されるため、無限リストを渡しても終了しないことがわかります...


0

私はHaskellを知りませんが、Schemeではfold-right常に最初にリストの最後の要素に「作用」します。したがって、循環リスト(無限リストと同じ)では機能しません。

fold-right末尾再帰的に記述できるかどうかはわかりませんが、循環リストではスタックオーバーフローが発生するはずです。 fold-leftOTOHは通常、末尾再帰で実装されており、早期に終了しないと無限ループに陥ります。


3
それは怠惰のためにHaskellでは異なります。
Lifu Huang
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.