згорнути проти поведінки складки з нескінченними списками


124

Код функції myAny в цьому запитанні використовує foldr. Він припиняє обробку нескінченного списку, коли предикат задоволений.

Я переписав його за допомогою foldl:

myAny :: (a -> Bool) -> [a] -> Bool
myAny p list = foldl step False list
   where
      step acc item = p item || acc

(Зверніть увагу, що аргументи функції кроків правильно відмінені.)

Однак він більше не припиняє обробку нескінченних списків.

Я намагався простежити виконання функції, як у відповіді Апокаліпса :

myAny even [1..]
foldl step False [1..]
step (foldl step False [2..]) 1
even 1 || (foldl step False [2..])
False  || (foldl step False [2..])
foldl step False [2..]
step (foldl step False [3..]) 2
even 2 || (foldl step False [3..])
True   || (foldl step False [3..])
True

Однак функція веде себе не так. Як це неправильно?

Відповіді:


231

Наскільки foldрізниця здається частим джерелом плутанини, ось ось більш загальний огляд:

Розглянемо складання списку з n значень [x1, x2, x3, x4 ... xn ]з деякою функцією fта насінням z.

foldl є:

  • Лівий асоціативний :f ( ... (f (f (f (f z x1) x2) x3) x4) ...) xn
  • Хвост рекурсивний : він повторюється через список, створюючи значення згодом
  • Ледачий : Нічого не оцінюється, поки результат не потрібен
  • Назад : foldl (flip (:)) []повертає список.

foldr є:

  • Правий асоціативний :f x1 (f x2 (f x3 (f x4 ... (f xn z) ... )))
  • Рекурсивний аргумент : Кожна ітерація стосується fнаступного значення та результату складання решти списку.
  • Ледачий : Нічого не оцінюється, поки результат не потрібен
  • Вперед : foldr (:) []повертає список незмінним.

Там є трохи тонкий момент тут , що поїздки люди до іноді: Тому що foldlце в зворотному напрямку кожного застосування fдодаються до зовнішній стороні результату; і тому, що він лінивий , нічого не оцінюють, поки не буде потрібен результат. Це означає, що для обчислення будь-якої частини результату Haskell спочатку повторює весь список, будуючи вираз вкладених додатків функції, потім оцінює найбільш віддалену функцію, оцінюючи її аргументи за потребою. Якщо fзавжди використовується перший аргумент, це означає, що Haskell повинен повторюватись до самого внутрішнього терміна, а потім працювати назад, обчислюючи кожну програму f.

Це, очевидно, далеко від ефективної хвостової рекурсії, яку знають і люблять більшість функціональних програмістів!

Насправді, навіть якщо foldlтехнічно є рекурсивним, оскільки весь вираз результату будується перед тим, як щось оцінити, це foldlможе спричинити переповнення стека!

З іншого боку, врахуйте foldr. Це також ліниво, але оскільки він працює вперед , кожне застосування fдодається до внутрішньої частини результату. Отже, для обчислення результату Haskell будує єдину функціональну програму, другий аргумент якої - решта складеного списку. Якщо, наприклад f, лінь у другому аргументі - конструкторі даних - результат буде поступово ледачим , при цьому кожен крок складки обчислюється лише тоді, коли буде оцінена деяка частина результату, яка потребує цього.

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

Отже, два важливі моменти, які слід зазначити, це:

  • foldr може перетворити одну ледачу рекурсивну структуру даних в іншу.
  • В іншому випадку ліниві складки зіткнуться з переповненням стека у великих чи нескінченних списках.

Можливо, ви помітили, що це здається, що foldrможна робити все foldl, плюс більше. Це правда! Насправді, foldl майже марний!

Але що робити, якщо ми хочемо отримати не лінивий результат, склавши над великим (але не нескінченним) списком? Для цього ми хочемо сувору складку , яку стандартні бібліотеки хоч і надають :

foldl' є:

  • Лівий асоціативний :f ( ... (f (f (f (f z x1) x2) x3) x4) ...) xn
  • Хвост рекурсивний : він повторюється через список, створюючи значення згодом
  • Суворо : кожне застосування функції оцінюється по ходу
  • Назад : foldl' (flip (:)) []повертає список.

Тому що foldl'це суворе , щоб обчислити результат Haskell буде оцінювати f на кожному кроці, замість того , щоб дозволити лівий аргумент накопичити величезний, невичісленного вираз. Це дає нам звичайну та ефективну хвостову рекурсію, яку ми хочемо! Іншими словами:

  • foldl' може складати великі списки ефективно.
  • foldl' буде висіти в нескінченному циклі (не викликати переповнення стека) у нескінченному списку.

У вікі Haskell є також сторінка, де це обговорюється .


6
Я прийшов сюди , тому що мені цікаво , чому foldrце краще , ніж foldlв Haskell , а навпаки , в Erlang (який я дізнався , перш ніж Haskell ). Оскільки Ерланг не лінивий і функції не криються , то foldlв Ерланге поводиться так, як foldl'вище. Це чудова відповідь! Гарна робота і спасибі!
Siu Ching Pong -Asuka Kenji-

7
Це здебільшого чудове пояснення, але я вважаю опис foldlяк "відсталим", так і foldr"вперед" проблематичним. Це частково тому flip, що застосовується на (:)ілюстрації того, чому складка відстала назад. Природна реакція - "звичайно, це відстала: ви flipпедите список конкатенації!" Також дивно бачити, що називається "назад", оскільки foldlзастосовується fдо елемента першого списку першим (найпотаємнішим) у повній оцінці. Це те, foldrщо "біжить назад", застосовуючи fспочатку останній елемент.
Дейв Абрахамс

1
@DaveAbrahams: Між справедливим foldlі foldrігноруванням строгості та оптимізацій, спочатку мається на увазі "зовнішній", а не "внутрішній". Ось чому foldrможна обробляти нескінченні списки, а foldlне можуть - права складання спочатку застосовується fдо першого елемента списку та (неоціненого) результату складання хвоста, тоді як ліва складка повинна пройти весь список, щоб оцінити найбільш віддалене застосування f.
CA McCann

1
Мені просто цікаво, чи є якийсь екземпляр, де foldl був би кращим перед foldl ', ти вважаєш, що є такий?
kazuoua

1
@kazuoua, де лінь є істотною, наприклад last xs = foldl (\a z-> z) undefined xs.
Буде Несс

28
myAny even [1..]
foldl step False [1..]
foldl step (step False 1) [2..]
foldl step (step (step False 1) 2) [3..]
foldl step (step (step (step False 1) 2) 3) [4..]

тощо.

Інтуїтивно зрозуміло, що foldlзавжди знаходиться на "зовні" або "зліва", тому воно спочатку розширюється. Ad infinitum.


10

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


0

Я не знаю Haskell, але в Scheme fold-rightзавжди завжди буде діяти на останній елемент списку. Таким чином, це не буде працювати для циклічного списку (який такий же, як і нескінченний).

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


3
У Хаскелла по-іншому через лінь.
Lifu Huang
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.