web-dev-qa-db-ja.com

左折を使用するタイミングと右折を使用するタイミングをどのように知っていますか?

左折が左傾のツリーを生成し、右折が右折のツリーを生成することを知っていますが、折り目に手を伸ばすと、頭痛を引き起こす思考で動揺して、どの種類の折り目を決定しようとするのかが時々わかります適切です。私は通常、問題全体を解き、問題に当てはまるようにfold関数の実装をステップスルーします。

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

  • 左に折り畳むか、右に折り畳むかを決定するためのいくつかの経験則は何ですか?
  • 直面している問題を考慮して、どのタイプのフォールドを使用するかを迅速に決定するにはどうすればよいですか?

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

(関数型)プログラミングでは折りたたみはそのような一般的なアクションなので、このような種類の決定を迅速かつ自信を持って行えるようにしたいと思います。だから...ヒントはありますか?

94
Jeff

折り畳みを中置演算子表記に変換できます(間に書き込む)。

この例は、アキュムレーター関数xを使用してフォールドします

fold x [A, B, C, D]

したがって等しい

A x B x C x D

ここで、演算子の結合性について考える必要があります(括弧を付けて!)。

left-associative演算子がある場合、このように括弧を設定します

((A x B) x C) x D

ここでは、左折りを使用します。例(haskellスタイルの擬似コード)

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

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

A x (B x (C x D))

例:Cons-Operator

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

一般に、算術演算子(ほとんどの演算子)は左結合であるため、foldlがより普及しています。しかし、他のケースでは、中置表記法+括弧が非常に便利です。

99
Dario

Olin Shivers 「foldlは基本的なリスト反復子」、「foldrは基本的なリスト再帰演算子」と言うことでそれらを区別しました。 foldlの仕組みを見ると:

((1 + 2) + 3) + 4

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

1 + (2 + (3 + 4))

ここで、ベースケース4への移動を確認し、そこから結果を構築できます。

だから私は経験則を仮定します:それが末尾反復形式で書くのが簡単なリスト反復のように見えるなら、foldlが行く方法です。

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

60
Steven Huwig

他のポスターは良い答えを与えており、私は彼らがすでに言ったことを繰り返しません。あなたの質問でScalaの例を挙げたので、私はScalaの特定の例を挙げます。 トリックfoldRightは_n-1_スタックフレームを保持する必要があります。ここで、nはリストの長さであり、これによりスタックオーバーフローが発生しやすくなります。この。

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として厳密に評価された言語では、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..._

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

28
Flaviu Cipcigan

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

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

foldr:(1 + (2 + (3 + 4)))は、1 + ?2 + ?のスタックフレームを開いてから3 + 4を計算する必要があります。その後、戻ってそれぞれの計算を行う必要があります。

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

4
Tricks