FoldLeft v. foldRight - это важно?
Ранее Николас Ринаудо ответил на мой вопрос о Scala Список foldRight Always Using foldLeft?
Изучая Haskell в настоящее время, я понимаю, что foldRight
должен быть предпочтительнее foldLeft
в случаях, когда ::
(preend) можно использовать над ++
(append).
Причиной, как я понимаю, является производительность - первое происходит в O(1)
, т.е. добавляет элемент к фронту - постоянное время. В то время как для последнего требуется O(N)
, т.е. Пройти весь список и добавить элемент.
В Scala, учитывая, что foldLeft
реализуется в терминах foldRight
, имеет ли значение использование :+
над ++
с foldRight
значение, поскольку foldRight
обращается вспять, а затем foldLeft'd
?
В качестве примера рассмотрим эту простую операцию 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
?
Ответы
Ответ 1
Ответ @Rein Henrichs действительно не имеет отношения к Scala, потому что Scala реализация foldLeft
и foldRight
полностью отличается (для стартеров Scala имеет нетерпеливую оценку).
foldLeft
и foldRight
сами по себе очень мало что делают для выполнения вашей программы. Оба являются (либерально говоря) O (n * c_f), где c_f - сложность одного вызова функции f
, которая задана. foldRight
медленнее на постоянный коэффициент из-за дополнительного reverse
.
Таким образом, реальный фактор, который отличает один от другого, - это сложность анонимной функции, которую вы даете. Иногда проще написать эффективную функцию, предназначенную для работы с foldLeft
, а иногда и с foldRight
. В вашем примере лучше использовать версию foldRight
, так как анонимная функция, которую вы даете foldRight
, равна O (1). Напротив, анонимная функция, которую вы даете foldLeft
, сама O (n) (амортизируется, что здесь важно), потому что acc
продолжает расти от 0 до n-1 и добавляет к списку n элементов O (n).
Поэтому на самом деле важно выбрать foldLeft
или foldRight
, но не из-за самих этих функций, а из-за анонимных функций, данных им. Если оба эквивалентны, выберите foldLeft
по умолчанию.
Ответ 2
Я могу дать ответ для 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. Это имеет несколько важных последствий:
-
Если f
- конструктор данных, этот конструктор данных будет самым левым, самым внешним с foldr. Это означает, что foldr реализует охраняемую рекурсию, так что возможно следующее:
> take 5 . foldr (:) [] $ [1..]
[1,2,3,4]
Это означает, что, например, foldr может быть как хорошим производителем, так и хорошим потребителем для коротких фьюжн. (Да, foldr (:) []
- тождественный морфизм для списков.)
Это невозможно с помощью foldl, потому что конструктор будет внутри рекурсивного вызова foldl и не может быть сопоставлен с образцом.
-
И наоборот, поскольку рекурсивный вызов foldl находится в самом левом, крайнем положении, он будет уменьшаться ленивой оценкой и не займет места в стеке совпадения шаблонов. В сочетании с соответствующей аннотацией строгости (например, foldl'
) это позволяет выполнять функции, такие как sum
или length
, в постоянном пространстве.
Подробнее об этом см. в ленивой оценке Haskell.
Ответ 3
На самом деле имеет значение, используете ли вы foldLeft
или foldRight
в Scala, по крайней мере, в списках, по крайней мере, в реализации по умолчанию. Я считаю, что этот ответ не относится к библиотекам, таким как скалаз.
Если вы посмотрите на исходный код foldLeft
и foldRight
для LinearSeqOptimized, вы увидите, что:
-
foldLeft
реализуется с помощью циклических и локальных изменяемых переменных и помещается в один стек кадров.
-
foldRight
рекурсивный, но не хвостовой рекурсивный и, следовательно, потребляет один стек стека за элемент в списке.
foldLeft
, таким образом, безопасен, а foldRight
может переполнять переполнение для длинных списков.
Изменить
Чтобы закончить мой ответ, поскольку он только затрагивает часть вашего вопроса: также важно, какой из них вы используете в зависимости от того, что вы намерены делать.
Чтобы воспользоваться вашим примером, я считаю, что оптимальным решением является использование foldLeft
, добавление результатов к вашему аккумулятору и reverse
результат.
Таким образом:
- вся операция O (n)
- он не будет переполнять стек, независимо от размера списка
Это, по сути, то, что, по вашему мнению, вы делали с foldRight
, если предположить, что он был реализован в терминах foldLeft
.
Если вы используете foldRight
, вы получите немного более быструю реализацию (ну, немного... в два раза быстрее, на самом деле, но все же O (n)) за счет безопасности.
Можно утверждать, что если вы знаете, что ваши списки будут достаточно маленькими, нет проблем с безопасностью, и вы можете использовать foldRight
. Я чувствую, но это только мнение, что если ваши списки достаточно малы, что вам не нужно беспокоиться о вашем стеке, они достаточно малы, чтобы вам не приходилось беспокоиться о производительности.
Ответ 4
От этого зависит:
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
Ответ 5
Я не эксперт Scala, но в Haskell одна из самых важных отличительных черт между foldl'
(которая действительно должна быть левой клавишей по умолчанию) и foldr
заключается в том, что foldr
будет работать на бесконечном структуры данных, где foldl'
будет вешать бесконечно.
Чтобы понять, почему это так, я рекомендую посетить foldl.com и foldr.com, несколько раз расширяя оценки и восстанавливая дерево вызовов. Вы быстро увидите, где foldr
соответствует сравнению с foldl'
.