以前は、 Nicolas Rinaudo がScalaの List foldRightで常に私の質問に回答しましたfoldLeftを常に使用していますか?
現在Haskellを勉強していますが、::
(前置)を++
(追加)よりも使用できる場合、foldRight
をfoldLeft
よりも優先する必要があることを理解しています。
私が理解しているように、理由はパフォーマンスです-前者はO(1)
で発生します。つまり、アイテムを前に追加します-一定の時間。一方、後者にはO(N)
が必要です。つまり、リスト全体を調べてアイテムを追加します。
Scalaでは、foldLeft
がfoldRight
の観点から実装されている場合、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では、foldLeft
がfoldRight
を使用する場合、どのfoldRight
またはfoldRight
を使用するかが重要ですか?
@Rein Henrichsの答えは、ScalaのfoldLeft
とfoldRight
の実装が完全に異なるため、実際にはScalaとは無関係です(最初に、Scalaは熱心な評価を持っています)。
foldLeft
とfoldRight
自体は、実際にはプログラムのパフォーマンスにほとんど影響しません。どちらも(自由に言えば)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)です。
つまり、実際にはmattersfoldLeft
とfoldRight
のどちらを選択しても、これらの関数自体のためではなく、それらに与えられた匿名関数のためです。両方が同等の場合は、デフォルトでfoldLeft
を選択します。
私は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つの重要な意味があります。
f
がデータコンストラクターの場合、そのデータコンストラクターは左端にあり、最も外側にフォルダーがあります。これは、foldrが guarded recursion を実装することを意味し、次のことが可能になります。
_> take 5 . foldr (:) [] $ [1..]
[1,2,3,4]
_
これは、たとえば、folderは ショートカットフュージョン の優れたプロデューサーと優れたコンシューマーの両方になることができることを意味します。 (はい、foldr (:) []
はリストの恒等射です。)
これはfoldlでは不可能です。コンストラクターはfoldlへの再帰呼び出しの内部にあり、パターンマッチングができないためです。
逆に、foldlの再帰呼び出しは左端の最も外側の位置にあるため、遅延評価によって削減され、パターンマッチングスタックのスペースを占有しません。適切な厳密性注釈(例:_foldl'
_)と組み合わせると、sum
やlength
などの関数を一定のスペースで実行できます。
これについての詳細は、 Haskellの遅延評価 を参照してください。
実際、少なくともデフォルトの実装では、ScalaでfoldLeft
を使用するかfoldRight
を使用するか、少なくともリストを使用するかどうかは重要です。ただし、この回答はscalazなどのライブラリには当てはまらないと思います。
LinearSeqOptimized のfoldLeft
およびfoldRight
のソースコードを見ると、次のことがわかります。
foldLeft
はループとローカルの可変変数で実装され、1つのスタックフレームに収まります。foldRight
は再帰的ですが、末尾再帰的ではないため、リストの要素ごとに1つのスタックフレームを消費します。したがって、foldLeft
は安全ですが、foldRight
は長いリストのスタックオーバーフローを引き起こす可能性があります。
編集質問の一部しか取り上げていないため、私の回答を完了するには、何をするかによって、どちらを使用するかが重要になります。
あなたの例を考えると、私が最適な解決策と考えるのは、foldLeft
を使用し、結果をアキュムレータの前に付加し、結果をreverse
にすることです。
こちらです:
これは、foldRight
の観点から実装されていると想定した場合、基本的にfoldLeft
を使用して行っていたと思っていたものです。
foldRight
を使用する場合は、実装がわずかに高速になります(まあ、わずかに2倍速くなりますが、実際にはO(n))が犠牲になります安全の。
リストが十分に小さくなることがわかっている場合、安全上の問題はなく、foldRight
を使用できると主張することができます。私は感じていますが、それは意見にすぎません。リストが小さすぎてスタックを気にする必要がない場合は、リストが小さすぎてパフォーマンスヒットを心配する必要がないと思います。
場合によっては、次の点を考慮してください。
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)
ご覧のように、foldLeft
はhead
から最後の要素までリストを走査します。一方、foldRight
は、最後の要素からhead
までトラバースします。
集約にフォールディングを使用する場合、違いはありません。
scala> l.foldLeft(0) { (acc, ele) => ele + acc }
res2: Int = 6
scala> l.foldRight(0) { (ele, acc) => ele + acc }
res3: Int = 6
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