Как вы знаете, когда использовать fold-left и когда использовать fold-right?

Я знаю, что fold-left создает левые деревья, а fold-right - деревья с правым склоном, но когда я достигаю складки, я иногда оказываюсь увязшим в головокружающей мысли, пытаясь определить, какие вид складки является подходящим. Обычно я заканчиваю разворачивание всей проблемы и перехожу к реализации функции fold, так как это относится к моей проблеме.

Так что я хочу знать:

  • Каковы некоторые эмпирические правила для определения того, нужно ли складывать влево или складывать вправо?
  • Как я могу быстро решить, какой тип сгиб использовать, учитывая проблему, с которой я сталкиваюсь?

В Scala в качестве примера (PDF) используется пример использования функции fold, чтобы написать функцию, называемую flatten, которая объединяет список списки элементов в один список. В этом случае правильная складка является правильным выбором (учитывая способ объединения списков), но мне пришлось немного подумать об этом, чтобы прийти к такому выводу.

Поскольку сгибание является таким общим действием в (функциональном) программировании, я бы хотел быстро и уверенно принимать такие решения. Итак... какие-нибудь советы?

Ответ 1

Вы можете перенести сгиб в нотацию оператора инфикса (запись между ними):

Этот пример складывается с использованием функции аккумулятора 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

Если ваш оператор является право-ассоциативным (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 является более распространенным. Но в других случаях примечание infix + круглые скобки весьма полезно.

Ответ 2

Olin Shivers дифференцировали их, сказав, что "foldl - это основной итератор списка", а "foldr - основной рекурсивный оператор списка". Если вы посмотрите, как работает foldl:

((1 + 2) + 3) + 4

вы можете увидеть, как строится аккумулятор (как в хвостовой рекурсивной итерации). Напротив, foldr продолжается:

1 + (2 + (3 + 4))

где вы можете увидеть обход базового футляра 4 и создать там результат.

Итак, я устанавливаю правило: если он похож на итерацию списка, которая была бы простой для записи в хвостовой рекурсивной форме, foldl - это путь.

Но на самом деле это, вероятно, будет наиболее очевидно из ассоциативности используемых вами операторов. Если они лево-ассоциативные, используйте foldl. Если они являются право-ассоциативными, используйте foldr.

Ответ 3

Другие плакаты дали хорошие ответы, и я не буду повторять то, что они уже сказали. Как вы указали пример Scala в своем вопросе, я приведу конкретный пример Scala. Как уже говорилось Tricks, 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. В противном случае пойдите с другими советами, приведенными в ответах;).

Ответ 4

Также стоит отметить (и я понимаю, что это указывает на очевидное), в случае коммутативного оператора два почти эквивалентны. В этой ситуации лучше всего выбрать foldl:

foldl: (((1 + 2) + 3) + 4) может вычислять каждую операцию и переносить накопленное значение вперед

foldr: (1 + (2 + (3 + 4))) должен быть открыт стек стека для 1 + ? и 2 + ? перед вычислением 3 + 4, тогда ему нужно вернуться и выполнить вычисления для каждого.

Мне не хватает эксперта по функциональным языкам или оптимизаторам компилятора, чтобы сказать, действительно ли это будет иметь значение, но, безусловно, кажется более чистым использовать foldl с коммутативными операторами.