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

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

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

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

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

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


person Jeff    schedule 18.09.2009    source источник
comment
выглядит похоже на stackoverflow.com/ questions / 384797 /   -  person Steven Huwig    schedule 19.09.2009
comment
Этот вопрос является более общим, чем тот, который касался конкретно Haskell. Лень имеет большое значение для ответа на вопрос.   -  person Chris Conway    schedule 19.09.2009
comment
Ой. Странно, почему-то мне показалось, что я увидел тег Haskell по этому вопросу, но, похоже, нет ...   -  person ephemient    schedule 19.09.2009


Ответы (4)


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

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

Если ваш оператор является правоассоциативным (правый угол), круглые скобки будут установлены следующим образом:

A x (B x (C x D))

Пример: Cons-Operator

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

В общем, арифметические операторы (большинство операторов) левоассоциативны, поэтому foldl более распространен. Но в остальных случаях весьма полезны инфиксные обозначения + круглые скобки.

person Dario    schedule 18.09.2009
comment
Что ж, то, что вы описали, на самом деле foldl1 и foldr1 в Haskell (foldl и foldr принимают внешнее начальное значение), а минусы Haskell называются (:), а не (::), но в остальном это правильно. Вы можете добавить, что Haskell дополнительно предоставляет _7 _ / _ 8_, которые являются строгими вариантами _9 _ / _ 10_, потому что ленивая арифметика не всегда желательна. - person ephemient; 19.09.2009
comment
Извините, мне показалось, что я заметил тег Haskell по этому вопросу, но его там нет. Мой комментарий действительно не имеет большого смысла, если это не Haskell ... - person ephemient; 19.09.2009
comment
@ephemient Вы это видели. Это псевдокод в стиле haskell. :) - person laughing_man; 21.04.2015
comment
Лучший ответ, который я когда-либо видел, касается разницы между складками. - person AleXoundOS; 16.01.2019

Олин Шиверс различал их, говоря, что «foldl - это основной итератор списка. "and" foldr является основным оператором рекурсии списка. " Если вы посмотрите, как работает foldl:

((1 + 2) + 3) + 4

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

1 + (2 + (3 + 4))

где вы можете увидеть переход к базовому случаю 4 и построение результата оттуда.

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

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

person Steven Huwig    schedule 18.09.2009

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

Поэтому мое практическое правило - для операторов, не обладающих определенной ассоциативностью, всегда используйте foldLeft, по крайней мере, в Scala. В противном случае используйте другие советы, приведенные в ответах;).

person Flaviu Cipcigan    schedule 19.09.2009
comment
Это было верно раньше, но в текущих версиях Scala foldRight была изменена для применения foldLeft к перевернутой копии списка. Например, в 2.10.3 github.com/scala/scala/blob/v2.10.3/src/library/scala/. Похоже, это изменение было внесено в начале 2013 года - github.com/scala/scala/commit/. - person Dhruv Kapoor; 05.09.2014

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

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

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

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

person Community    schedule 18.09.2009
comment
Дополнительные кадры стека определенно будут иметь значение для больших списков. Если ваши кадры стека превышают размер кеш-памяти процессора, то ваши промахи в кэше будут влиять на производительность. Если список не является двусвязным, сделать foldr хвостовой рекурсивной функцией довольно сложно, поэтому вам следует использовать foldl, если нет причин не делать этого. - person A. Levy; 19.09.2009
comment
Ленивый характер Haskell мешает этому анализу. Если сворачиваемая функция не является строгой по второму параметру, то foldr вполне может быть более эффективным, чем foldl, и не потребует дополнительных кадров стека. - person ephemient; 19.09.2009
comment
Извините, мне показалось, что я заметил тег Haskell по этому вопросу, но его там нет. Мой комментарий действительно не имеет большого смысла, если это не Haskell ... - person ephemient; 19.09.2009