web-dev-qa-db-ja.com

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

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

Foldlを使用して書き直しました。

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

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

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

Apocalisp's answer のように関数の実行をトレースしようとしました。

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

ただし、これは関数の動作方法ではありません。これはどうですか?

110
titaniumdecoy

foldsがどのように異なるかは、混乱の原因となることが多いため、より一般的な概要を次に示します。

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

foldlは:

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

foldrは:

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

ここには時々人をつまずかせる微妙なポイントがあります:foldlbackwardsであるため、fの各アプリケーションはoutsideに追加されます結果;また、lazyであるため、結果が必要になるまで何も評価されません。つまり、結果の任意の部分を計算するために、Haskellは最初にentire listを反復処理して、ネストされた関数アプリケーションの式を構築し、次にoutermost関数、必要に応じて引数を評価します。 fが常に最初の引数を使用する場合、これはHaskellが最も内側の用語まで再帰し、fの各アプリケーションを逆方向に計算する必要があることを意味します。

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

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

一方、foldrを検討してください。また、怠、ですが、forwardsを実行するため、fの各アプリケーションはinsideに追加されます結果。そのため、結果を計算するために、Haskellはsingle関数アプリケーションを構築します。その2番目の引数は折り畳まれたリストの残りの部分です。 fの2番目の引数(データコンストラクターなど)が遅延している場合、結果はincrementally lazyになり、フォールドの各ステップが計算されます結果の必要な部分が評価される場合のみ。

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

そのため、注意すべき2つの重要なポイントは次のとおりです。

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

foldrは、foldlができることのすべてに加えて、さらに多くのことができるように聞こえます。これは本当です!実際、foldlはほとんど役に立ちません!

しかし、大きな(ただし無限ではない)リストを折り返すことで遅延のない結果を生成する場合はどうでしょうか。このために、strict foldが必要です。これは 標準ライブラリが提供する

foldl'は:

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

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

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

Haskell wikiには これについて議論しているページ もあります。

215
C. A. McCann
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は常に「外側」または「左側」にあるため、最初に展開されます。広告無限。

26
Artelius

Haskellのドキュメントで見ることができます here foldlは末尾再帰であり、無限のリストが渡されると終了しません。値を返す前に次のパラメーターで自身を呼び出すためです...

10
Romain

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

fold-rightは末尾再帰で記述できますが、循環リストの場合はスタックオーバーフローが発生します。 fold-left OTOHは通常、末尾再帰で実装されており、早期に終了しない場合、無限ループに陥ります。

0
leppie