fold-leftを使用するタイミングとfold-rightを使用するタイミングをどのようにして知っていますか?


99

fold-leftは左寄りのツリーを生成し、fold-rightは右寄りのツリーを生成することを認識していますが、フォールドに到達すると、どの種類のフォールドを決定しようとして頭痛を誘発する思考に行き詰まることがあります。適切です。私は通常、問題全体をほどき、問題に当てはまるようにfold関数の実装を段階的に実行します。

だから私が知りたいのは:

  • 左に折りたたむか右に折りたたむかを決定するための経験則は何ですか?
  • 私が直面している問題を踏まえて、どのタイプの折り目を使用するかをすばやく決定するにはどうすればよいですか?

要素リストのリストを単一のリストに連結するflattenと呼ばれる関数をフォールドを使用して記述する例がScala by Example(PDF)にあります。その場合、正しいリストは適切な選択です(リストを連結する方法を考えると)が、その結論に達するには少し考えなければなりませんでした。

(関数型)プログラミングではフォールディングが非常に一般的なアクションであるため、この種の決定を迅速かつ自信を持って行えるようにしたいと思います。だから...何かヒントはありますか?



1
このQはそれよりも一般的で、具体的にはHaskellに関するものでした。怠惰は質問への答えに大きな違いをもたらします。
Chris Conway、

ああ。奇妙なことに、どういうわけか私はこの質問にHaskellタグを見たと思ったが、私はそうは思わない...
ephemient

回答:


105

フォールドを中置演算子表記に変換できます(その間に書き込みます)。

この例では、アキュムレータ関数を使用して折り畳みます x

fold x [A, B, C, D]

したがって等しい

A x B x C x D

次に、演算子の結合性について(括弧を入れて)推論する必要があります。

あなたが持っている場合は左結合演算子を、あなたはこのような括弧を設定します

((A x B) x C) x D

ここでは、使用左倍に。例(haskellスタイルの疑似コード)

foldl (-) [1, 2, 3] == (1 - 2) - 3 == 1 - 2 - 3 // - is left-associative

演算子が右結合(右折り)の場合、括弧は次のように設定されます。

A x (B x (C x D))

例:Cons-Operator

foldr (:) [] [1, 2, 3] == 1 : (2 : (3 : [])) == 1 : 2 : 3 : [] == [1, 2, 3]

一般に、算術演算子(ほとんどの演算子)は左結合であるためfoldl、より広く使用されています。しかし、それ以外の場合、中置表記+括弧は非常に便利です。


6
まあ、あなたが説明したものは実際foldl1foldr1はHaskellにあり(foldlそしてfoldr外部の初期値を取ります)、Haskellの "cons"は(:)not と呼ばれ(::)ますが、それ以外の場合はこれが正しいです。Haskell が/の厳密な変形であるfoldl'/ foldl1'を追加で提供することを追加したいかもしれません。なぜなら、遅延算術は常に望ましいとは限らないからです。foldlfoldl1
ephemient 2009

申し訳ありませんが、この質問に「Haskell」タグが表示されたと思いましたが、ありません。私のコメントは、Haskell
でなければ

@ephemientあなたはそれを見ました。「ハスケル風の疑似コード」です。:)
laughing_man

折り目の違いに関連してこれまでに見た中での最良の答え。
AleXoundOS

60

Olin Shiversは、「foldlは基本的なリスト反復子」と「folderは基本的なリスト再帰演算子」と言って、それらを区別しました。foldlのしくみを見ると、次のようになります。

((1 + 2) + 3) + 4

アキュムレータが(末尾再帰反復のように)構築されているのがわかります。対照的に、foldrは続行します。

1 + (2 + (3 + 4))

ここでは、ベースケース4へのトラバーサルを確認し、そこから結果を構築しています。

だから私は経験則を当てはめます:それがリスト反復のように見える場合、テール再帰形式で書くのが簡単なものであるなら、foldlが進むべき道です。

しかし、実際には、これはおそらく、使用している演算子の結合性から最も明白になります。左結合である場合は、foldlを使用します。それらが右結合である場合、foldrを使用します。


29

他のポスターは良い答えを与えてくれました、そして私は彼らがすでに言ったことを繰り返さないでしょう。質問でScalaの例を示したので、Scala固有の例を示します。トリックはすでに述べ、foldRight維持する必要があるn-1場合には、スタックフレームをnおよびもない尾再帰はここからあなたを救うことができる-あなたのリストの長さであり、これは簡単にスタックオーバーフローにつながることができます。

A List(1,2,3).foldRight(0)(_ + _)は次のようになります。

1 + List(2,3).foldRight(0)(_ + _)        // first stack frame
    2 + List(3).foldRight(0)(_ + _)      // second stack frame
        3 + 0                            // third stack frame 
// (I don't remember if the JVM allocates space 
// on the stack for the third frame as well)

が次のList(1,2,3).foldLeft(0)(_ + _)ように減少します:

(((0 + 1) + 2) + 3)

これは、の実装でList行われるように、繰り返し計算できます。

Scalaとして厳密に評価された言語では、a foldRightは大きなリストのスタックを簡単に爆破できますが、foldLeftはありません。

例:

scala> List.range(1, 10000).foldLeft(0)(_ + _)
res1: Int = 49995000

scala> List.range(1, 10000).foldRight(0)(_ + _)
java.lang.StackOverflowError
        at scala.List.foldRight(List.scala:1081)
        at scala.List.foldRight(List.scala:1081)
        at scala.List.foldRight(List.scala:1081)
        at scala.List.foldRight(List.scala:1081)
        at scala.List.foldRight(List.scala:1081)
        at scala.List.foldRight(List.scala:1081)
        at scala.List.foldRight(List.scala:1081)
        at scala.List.foldRight(List.scala:1081)
        at scala.List.foldRig...

したがって、私の経験則は、特定の結合性を持たない演算子の場合foldLeft、少なくともScala では常にを使用します。それ以外の場合は、回答に記載されている他のアドバイスを使用してください;)。


13
これは以前にも当てはまりましたが、Scalaの現在のバージョンでは、foldRightが変更され、リストの反転コピーにfoldLeftが適用されました。たとえば、2.10.3では、github.com/scala/scala/blob/v2.10.3/src/library/scala/…です。この変更は2013年の初めに行われたようです-github.com/scala/scala/commit/…
Dhruv Kapoor 2014

4

また、注目に値します(そして、これは明らかなことを少し述べていることを私は理解しています)。可換演算子の場合、2つはほとんど同じです。この状況では、foldlの方が適切な選択です。

foldl: (((1 + 2) + 3) + 4)各操作を計算し、累積値を繰り上げることができます

foldr: (1 + (2 + (3 + 4)))スタックフレームがためにオープンする必要がある1 + ?2 + ?計算する前に3 + 4、それは戻って、それぞれの計算を行う必要があります。

関数型言語やコンパイラの最適化の専門家では、これが実際に違いをもたらすかどうかを説明するのに十分ではありませんが、可換演算子でfoldlを使用する方が確かにきれいです。


1
余分なスタックフレームは、大きなリストでは間違いなく違いがあります。スタックフレームがプロセッサのキャッシュのサイズを超える場合、キャッシュミスがパフォーマンスに影響します。リストが二重にリンクされていない限り、foldrを末尾再帰関数にするのはちょっと難しいので、特に理由がない限り、foldlを使用してください。
A.レビー

4
Haskellの怠惰な性質は、この分析を混乱させます。折りたたまれている関数が2番目のパラメーターで非厳密であるfoldr場合はfoldl、よりも効率的であり、追加のスタックフレームを必要としません。

2
申し訳ありませんが、この質問に「Haskell」タグが表示されたと思いましたが、ありません。私のコメントは、Haskell
でなければ
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.