web-dev-qa-db-ja.com

foldLeft v。foldRight-それは重要ですか?

以前は、 Nicolas Rinaudo がScalaの List foldRightで常に私の質問に回答しましたfoldLeftを常に使用していますか?

現在Haskellを勉強していますが、::(前置)を++(追加)よりも使用できる場合、foldRightfoldLeftよりも優先する必要があることを理解しています。

私が理解しているように、理由はパフォーマンスです-前者はO(1)で発生します。つまり、アイテムを前に追加します-一定の時間。一方、後者にはO(N)が必要です。つまり、リスト全体を調べてアイテムを追加します。

Scalaでは、foldLeftfoldRightの観点から実装されている場合、foldRightが逆になり、次に:+になるため、++foldLeft'dとともにfoldRightで使用する利点は重要です。

例として、リストの要素を順番に返すだけの単純なfold..操作を考えます。

foldLeftは各要素を折りたたみ、各項目を:+を介してリストに追加します。

scala> List("foo", "bar").foldLeft(List[String]()) { 
                                                    (acc, elem) => acc :+ elem }
res9: List[String] = List(foo, bar)

foldRightは、各項目で::演算子を使用してfoldLeftを実行しますが、その後逆になります。

scala> List("foo", "bar").foldRight(List[String]()) { 
                                                    (elem, acc) => elem :: acc }
res10: List[String] = List(foo, bar)

実際には、Scalaでは、foldLeftfoldRightを使用する場合、どのfoldRightまたはfoldRightを使用するかが重要ですか?

21
Kevin Meredith

@Rein Henrichsの答えは、ScalaのfoldLeftfoldRightの実装が完全に異なるため、実際にはScalaとは無関係です(最初に、Scalaは熱心な評価を持っています)。

foldLeftfoldRight自体は、実際にはプログラムのパフォーマンスにほとんど影響しません。どちらも(自由に言えば)O(n * c_f)です。c_fは、指定された関数fへの1回の呼び出しの複雑さです。ただし、foldRightは、reverseが追加されているため、定数係数により遅くなります。

したがって、1つをもう1つと区別する実際の要因は、指定する匿名関数の複雑さです。場合によっては、foldLeftで機能するように設計された効率的な関数を書く方が簡単な場合があります。foldRightを使用する場合もあります。あなたの例では、foldRightに与える匿名関数がO(1)であるため、foldRightバージョンが最適です。これとは対照的に、foldLeftが0からn-1に増え続け、次のように追加されるため、accに指定する無名関数はO(n)それ自体が重要です) n個の要素のリストはO(n)です。

つまり、実際にはmattersfoldLeftfoldRightのどちらを選択しても、これらの関数自体のためではなく、それらに与えられた匿名関数のためです。両方が同等の場合は、デフォルトでfoldLeftを選択します。

38
sjrd

私はHaskellに回答を提供することはできますが、それがScalaに関連することはないと思います。

両方のソースから始めましょう。

_foldl f z []     = z
foldl f z (x:xs) = foldl f (f z x) xs

foldr f z []     = z
foldr f z (x:xs) = f x (foldr f z xs)
_

次に、foldlまたはfoldrへの再帰呼び出しが右側のどこに表示されるかを見てみましょう。 foldlの場合、最も外側にあります。 foldrの場合、fのアプリケーション内にあります。これには2つの重要な意味があります。

  1. fがデータコンストラクターの場合、そのデータコンストラクターは左端にあり、最も外側にフォルダーがあります。これは、foldrが guarded recursion を実装することを意味し、次のことが可能になります。

    _> take 5 . foldr (:) [] $ [1..]
    [1,2,3,4]
    _

    これは、たとえば、folderは ショートカットフュージョン の優れたプロデューサーと優れたコンシューマーの両方になることができることを意味します。 (はい、foldr (:) []はリストの恒等射です。)

    これはfoldlでは不可能です。コンストラクターはfoldlへの再帰呼び出しの内部にあり、パターンマッチングができないためです。

  2. 逆に、foldlの再帰呼び出しは左端の最も外側の位置にあるため、遅延評価によって削減され、パターンマッチングスタックのスペースを占有しません。適切な厳密性注釈(例:_foldl'_)と組み合わせると、sumlengthなどの関数を一定のスペースで実行できます。

これについての詳細は、 Haskellの遅延評価 を参照してください。

12
Rein Henrichs

実際、少なくともデフォルトの実装では、ScalaでfoldLeftを使用するかfoldRightを使用するか、少なくともリストを使用するかどうかは重要です。ただし、この回答はscalazなどのライブラリには当てはまらないと思います。

LinearSeqOptimizedfoldLeftおよびfoldRightのソースコードを見ると、次のことがわかります。

  • foldLeftはループとローカルの可変変数で実装され、1つのスタックフレームに収まります。
  • foldRightは再帰的ですが、末尾再帰的ではないため、リストの要素ごとに1つのスタックフレームを消費します。

したがって、foldLeftは安全ですが、foldRightは長いリストのスタックオーバーフローを引き起こす可能性があります。

編集質問の一部しか取り上げていないため、私の回答を完了するには、何をするかによって、どちらを使用するかが重要になります。

あなたの例を考えると、私が最適な解決策と考えるのは、foldLeftを使用し、結果をアキュムレータの前に付加し、結果をreverseにすることです。

こちらです:

  • 全体の操作はO(n)です
  • リストのサイズに関係なく、スタックをオーバーフローしません

これは、foldRightの観点から実装されていると想定した場合、基本的にfoldLeftを使用して行っていたと思っていたものです。

foldRightを使用する場合は、実装がわずかに高速になります(まあ、わずかに2倍速くなりますが、実際にはO(n))が犠牲になります安全の。

リストが十分に小さくなることがわかっている場合、安全上の問題はなく、foldRightを使用できると主張することができます。私は感じていますが、それは意見にすぎません。リストが小さすぎてスタックを気にする必要がない場合は、リストが小さすぎてパフォーマンスヒットを心配する必要がないと思います。

10
Nicolas Rinaudo

場合によっては、次の点を考慮してください。

scala> val l = List(1, 2, 3)
l: List[Int] = List(1, 2, 3)

scala> l.foldLeft(List.empty[Int]) { (acc, ele) => ele :: acc }
res0: List[Int] = List(3, 2, 1)

scala> l.foldRight(List.empty[Int]) { (ele, acc) => ele :: acc }
res1: List[Int] = List(1, 2, 3)

ご覧のように、foldLeftheadから最後の要素までリストを走査します。一方、foldRightは、最後の要素からheadまでトラバースします。

集約にフォールディングを使用する場合、違いはありません。

scala> l.foldLeft(0) { (acc, ele) => ele + acc }
res2: Int = 6

scala> l.foldRight(0) { (ele, acc) => ele + acc }
res3: Int = 6
5
tgr
scala> val words = List("Hic", "Est", "Index")
words: List[String] = List(Hic, Est, Index)

Infold of foldLeft:リスト要素は最初に空の文字列に追加され、その後に追加されます

words.foldLeft("")(_ + _) == (("" + "Hic") + "Est") + "Index"      //"HicEstIndex"

foldRightの場合:空の文字列は要素の最後に追加されます

words.foldRight("")(_ + _) == "Hic" + ("Est" + ("Index" + ""))     //"HicEstIndex"

どちらの場合も同じ出力が返されます

def foldRight[B](z: B)(f: (A, B) => B): B
def foldLeft[B](z: B)(f: (B, A) => B): B
2

私はScala=エキスパートではありませんが、Haskellでは、foldl'(実際にはデフォルトの左折りたたみであるべきです)とfoldrの間の最も重要な差別化機能の1つです。 foldrは無限のデータ構造で機能し、foldl'は無期限にハングします。

これがなぜであるかを理解するために、私は foldl.com および foldr.com にアクセスし、評価を数回展開し、コールツリーを再構築することをお勧めします。 foldrfoldl'と比較して適切な場所がすぐにわかります。

2
Benjamin Kovach