Как вы знаете, когда использовать 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 с коммутативными операторами.