Наслідки foldr vs. foldl (або foldl ')


155

По-перше, справжній світ Хаскелл , який я читаю, говорить, що ніколи не використовувати, foldlа замість цього використовувати foldl'. Тож я їй довіряю.

Але я туманно, коли використовувати foldrпроти foldl'. Хоча я бачу структуру того, як вони працюють по-різному, викладену переді мною, я занадто дурний, щоб зрозуміти, коли "що краще". Я думаю, мені здається, що насправді не має значення, який використовується, оскільки вони обидва дають однакову відповідь (чи не так?). Насправді, мій попередній досвід роботи з цією конструкцією походить від Ruby's injectі Clojure reduce, які, схоже, не мають "лівої" та "правої" версій. (Побічне запитання: яку версію вони використовують?)

Будемо дуже вдячні за будь-яке розуміння, яке може допомогти розумним завданням, як я!

Відповіді:


174

Рекурсія на те, foldr f x ysде ys = [y1,y2,...,yk]виглядає

f y1 (f y2 (... (f yk x) ...))

тоді як рекурсія для foldl f x ysвигляду виглядає так

f (... (f (f x y1) y2) ...) yk

Важлива відмінність тут полягає в тому, що якщо результат f x yможна обчислити, використовуючи лише значення x, тоді foldrне потрібно вивчати весь список. Наприклад

foldr (&&) False (repeat False)

повертає Falseтоді

foldl (&&) False (repeat False)

ніколи не припиняється. (Примітка: repeat Falseстворює нескінченний список, де є кожен елемент False.)

З іншого боку, foldl'хвіст рекурсивний і суворий. Якщо ви знаєте, що вам доведеться пройти весь список незалежно від того, що (наприклад, підсумовуючи числа в списку), тоді foldl'це більш просторово (і, мабуть, час) ефективніше, ніж foldr.


2
У foldr він оцінюється як f y1 thunk, тому він повертає False, однак у foldl f не може знати жодного з його параметрів. У Haskell, незалежно від того, чи є хвоста рекурсією чи ні, вона може викликати переповнення гронів, тобто thunk is занадто великий. foldl 'може зменшити обертання безпосередньо під час виконання.
Сойєр

25
Щоб уникнути плутанини, зауважте, що в дужках не відображається фактичний порядок оцінювання. Оскільки Хаскелл лінивий, спочатку будуть оцінені самі зовнішні вирази.
Лій

Зробіть відповідь. Я хотів би додати, що якщо ви хочете складку, яка може зупинити частину списку, ви повинні використовувати foldr; якщо я не помиляюся, ліві складки неможливо зупинити. (Ти натякаєш на це, коли кажеш, "якщо знаєш ... ти ... пройдеш весь список"). Крім того, помилка друку ", що використовує лише значення", повинна бути змінена на "використовуючи лише значення". Тобто видаліть слово "на". (Stackoverflow не дозволив мені внести зміни в 2 знаки!).
Lqueryvg

@Lqueryvg два способи зупинити ліву складку: 1. кодуйте її правою складкою (див. Див fodlWhile ); 2. перетворіть його в лівий скан ( scanl) і зупиніть це за допомогою last . takeWhile pабо подібного. Uh, і 3. використання mapAccumL. :)
Чи буде Несс

1
@Десті, тому що вона створює нову частину свого загального результату на кожному кроці - на відміну від цього foldl, який збирає загальний результат і виробляє його лише після того, як вся робота буде закінчена і більше кроків для виконання немає. Так, наприклад foldl (flip (:)) [] [1..3] == [3,2,1], так scanl (flip(:)) [] [1..] = [[],[1],[2,1],[3,2,1],...]... IOW, foldl f z xs = last (scanl f z xs)і нескінченні списки не мають останнього елемента (який у наведеному вище прикладі був би нескінченним списком, від INF до 1).
Буде Несс


33

Їх семантика відрізняється, тому ви не можете просто обмінятися foldlі foldr. Один складає елементи зліва, інший справа. Таким чином, оператор застосовується в іншому порядку. Це має значення для всіх неасоціативних операцій, таких як віднімання.

На Haskell.org є цікава стаття на цю тему.


Їх семантика відрізняється лише тривіальним способом, тобто безглуздо на практиці: Порядок аргументів використаної функції. Тож інтерфейс сприятливий, що вони все ще вважаються обмінними. Справжня різниця - це, здається, лише оптимізація / реалізація.
Evi1M4chine

2
@ Evi1M4chine Жодна з відмінностей не є тривіальною. Навпаки, вони є суттєвими (і, так, на практиці значущими). Насправді, якби я сьогодні писав цю відповідь, це ще більше підкреслило б цю різницю.
Конрад Рудольф

23

Незабаром, foldrкраще, коли функція акумулятора ледача на своєму другому аргументі. Детальніше читайте на Вікі Haskell's Stack Overflow (призначений каламбур).


18

Причина foldl', яку віддають перевагу foldl99% усіх застосувань, полягає в тому, що вона може працювати в постійному просторі для більшості застосувань.

Візьміть функцію sum = foldl['] (+) 0. При foldl'використанні сума одразу обчислюється, тому застосування sumдо нескінченного списку буде просто працювати назавжди і, швидше за все, у постійному просторі (якщо ви використовуєте такі речі, як Ints, Doubles, Floats. IntegerS, використовуватимуть більше, ніж постійний простір, якщо число стає більшим, ніж maxBound :: Int).

З foldl, накопичується гроно (як рецепт, як отримати відповідь, яку можна оцінити пізніше, а не зберігати відповідь). Ці гроти можуть зайняти багато місця, і в цьому випадку набагато краще оцінити вираз, ніж зберігати грудку (що веде до переповнення стека… і приведе вас до… о, неважливо)

Сподіваюся, що це допомагає.


1
Великим винятком є ​​те, що якщо передана функція foldlне робить нічого, крім застосування конструкторів до одного або декількох його аргументів.
dfeuer

Чи існує загальна закономірність, коли foldlнасправді найкращий вибір? (Як і нескінченні списки, коли foldrнеправильний вибір, оптимізація мудрі?)
Evi1M4chine

@ Evi1M4chine не впевнений, що ти маєш на увазі, foldrбудучи неправильним вибором для нескінченних списків. Насправді, ви не повинні використовувати foldlабо foldl'нескінченні списки. Дивіться вікі Haskell про переповнення стека
KevinOrr

14

До речі, Ruby's injectі Clojure reduceє foldl(або foldl1залежно від того, яку версію ви використовуєте). Зазвичай, коли в мові є лише одна форма, це ліва складка, включаючи Python's reduce, Perl's List::Util::reduce, C ++ 's accumulate, C #' s Aggregate, Smalltalk's inject:into:, PHP array_reduce, Mathematica ' Foldі т. Д. Поширені параметри Lisp reduceна лівій складці, але є можливість для правої складки.


2
Цей коментар корисний, але я буду вдячний джерелам.
titaniumdecoy

Звичайний Лісп reduceне лінивий, так що, foldl'і багато з цих міркувань тут не стосуються.
MicroVirus

Я думаю, ти маєш на увазі foldl', оскільки вони суворі мови, ні? Інакше це не означає, що всі ці версії викликають переповнення стека, як foldlце робиться?
Evi1M4chine

6

Як зазначає Конрад , їх семантика різна. Вони навіть не мають одного типу:

ghci> :t foldr
foldr :: (a -> b -> b) -> b -> [a] -> b
ghci> :t foldl
foldl :: (a -> b -> a) -> a -> [b] -> a
ghci> 

Наприклад, оператор список Append (++) може бути реалізований з допомогою, foldrяк

(++) = flip (foldr (:))

поки

(++) = flip (foldl (:))

дасть вам помилку типу.


Їх тип такий самий, щойно перемикається, що не має значення. Їх інтерфейс та результати однакові, за винятком неприємної проблеми P / NP (читайте: нескінченні списки). ;) Оптимізація завдяки впровадженню є єдиною різницею на практиці, наскільки я можу сказати.
Evi1M4chine

@ Evi1M4chine Це неправильно, подивіться на цей приклад: foldl subtract 0 [1, 2, 3, 4]оцінює до -10, а foldr subtract 0 [1, 2, 3, 4]оцінює до -2. foldlнасправді 0 - 1 - 2 - 3 - 4поки foldrє 4 - 3 - 2 - 1 - 0.
krapht

@krapht, foldr (-) 0 [1, 2, 3, 4]є -2і foldl (-) 0 [1, 2, 3, 4]є -10. З іншого боку, subtractце відстає від того, що ви могли очікувати ( subtract 10 14є 4), так foldr subtract 0 [1, 2, 3, 4]є -10і foldl subtract 0 [1, 2, 3, 4]є 2(позитивне).
pianoJames
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.