Я знаю, что fold-left создает деревья с наклоном влево, а fold-right создает деревья с наклоном вправо, но когда я тянусь к сгибу, я иногда зацикливаюсь на вызывающих головную боль мыслях, пытаясь определить, какой тип сгиба подходит. Обычно я закрываю всю проблему и перехожу к реализации функции сгиба применительно к моей проблеме.
Итак, я хочу знать:
- Какие эмпирические правила определяют, сбрасывать ли карты влево или вправо?
- Как я могу быстро решить, какой тип складки использовать, учитывая мою проблему?
В Scala by Example (PDF) есть пример использования свертки для написания функции под названием flatten, которая объединяет список списков элементов в один список. В этом случае правильное сгибание - правильный выбор (учитывая способ объединения списков), но мне пришлось немного подумать, чтобы прийти к такому выводу.
Поскольку сворачивание - такое обычное явление в (функциональном) программировании, я хотел бы иметь возможность принимать такие решения быстро и уверенно. Итак ... какие-нибудь советы?
Ответы:
Вы можете преобразовать свертку в нотацию инфиксного оператора (напишите между ними):
В этом примере сворачивание с использованием функции аккумулятора
x
таким образом равно
Теперь вам просто нужно подумать об ассоциативности вашего оператора (заключив скобки!).
Если у вас есть левоассоциативный оператор, вы установите круглые скобки следующим образом
Здесь вы используете левую складку . Пример (псевдокод в стиле haskell)
Если ваш оператор правоассоциативен ( правый угол ), круглые скобки будут установлены следующим образом:
Пример: Cons-Operator
В общем, арифметические операторы (большинство операторов) левоассоциативны, поэтому они
foldl
более распространены. Но в остальных случаях весьма полезны инфиксные обозначения + круглые скобки.источник
foldl1
иfoldr1
в Haskell (foldl
иfoldr
принимает внешнее начальное значение), а «минусы» Haskell(:)
не называются(::)
, но в остальном это правильно. Вы можете добавить, что Haskell дополнительно предоставляетfoldl'
/,foldl1'
которые являются строгими вариантамиfoldl
/foldl1
, потому что ленивая арифметика не всегда желательна.Олин Шиверс различил их, сказав, что «foldl - это основной итератор списка», а «foldr - это основной оператор рекурсии списка». Если вы посмотрите, как работает foldl:
вы можете увидеть, как создается аккумулятор (как в хвостовой рекурсивной итерации). Напротив, foldr продолжает:
где вы можете увидеть переход к базовому случаю 4 и построение результата оттуда.
Итак, я устанавливаю практическое правило: если это похоже на итерацию списка, такую, которую было бы просто написать в хвостовой рекурсивной форме, foldl - лучший вариант.
Но на самом деле это, вероятно, будет наиболее очевидно из ассоциативности используемых вами операторов. Если они левоассоциативны, используйте foldl. Если они правоассоциативны, используйте foldr.
источник
Другие плакаты дали хорошие ответы, и я не буду повторять то, что они уже сказали. Поскольку в своем вопросе вы привели пример Scala, я приведу конкретный пример Scala. Как уже сказал Трюк ,
foldRight
необходимо сохранитьn-1
кадры стека, гдеn
длина вашего списка, и это может легко привести к переполнению стека - и даже хвостовая рекурсия не может вас от этого спасти.А
List(1,2,3).foldRight(0)(_ + _)
уменьшится до:в то время как
List(1,2,3).foldLeft(0)(_ + _)
уменьшится до:который может быть вычислен итеративно, как это сделано в реализации
List
.В языке со строгой оценкой, таком как Scala, a
foldRight
может легко взорвать стек для больших списков, а afoldLeft
- нет.Пример:
Поэтому мое практическое правило - всегда использовать операторы, не обладающие определенной ассоциативностью
foldLeft
, по крайней мере, в Scala. В противном случае используйте другие советы, приведенные в ответах;).источник
Также стоит отметить (и я понимаю, что это немного констатирует очевидное), что в случае коммутативного оператора они в значительной степени эквивалентны. В этой ситуации фолдл может быть лучшим выбором:
foldl:
(((1 + 2) + 3) + 4)
может вычислять каждую операцию и переносить накопленное значение впередfoldr:
(1 + (2 + (3 + 4)))
требуется, чтобы фрейм стека был открыт для вычисления1 + ?
и2 + ?
перед вычислением3 + 4
, затем ему нужно вернуться и выполнить вычисление для каждого.Я недостаточно разбираюсь в функциональных языках или оптимизации компилятора, чтобы сказать, действительно ли это будет иметь значение, но, безусловно, кажется более чистым использовать foldl с коммутативными операторами.
источник
foldr
вполне может быть более эффективной, чемfoldl
и не потребует дополнительных кадров стека.