Чем отличаются fold
s, кажется, частый источник путаницы, поэтому вот более общий обзор:
Рассмотрите возможность свертывания списка из n значений [x1, x2, x3, x4 ... xn ]
с помощью некоторой функции f
и начального числа z
.
foldl
is:
- Левая ассоциативность:
f ( ... (f (f (f (f z x1) x2) x3) x4) ...) xn
- Хвостовая рекурсивная: выполняется итерация по списку, после чего создается значение.
- Ленивый: ничего не оценивается, пока не потребуется результат.
- Назад:
foldl (flip (:)) []
переворачивает список.
foldr
is:
- Правоассоциативный:
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'
is:
- Левая ассоциативная:
f ( ... (f (f (f (f z x1) x2) x3) x4) ...) xn
- Хвостовая рекурсивная: выполняется итерация по списку, после чего создается значение.
- Строгое: каждое приложение-функция оценивается по ходу выполнения.
- Назад:
foldl' (flip (:)) []
переворачивает список.
Поскольку foldl'
является строгим, для вычисления результата Haskell будет оценивать f
на каждом шаге, вместо того, чтобы позволять левому аргументу накапливать огромное невычисленное выражение. Это дает нам обычную, эффективную хвостовую рекурсию, которую мы хотим! Другими словами:
foldl'
может эффективно складывать большие списки.
foldl'
будет зависать в бесконечном цикле (не вызывая переполнения стека) в бесконечном списке.
В вики-странице Haskell также есть страница, на которой это обсуждается.
person
C. A. McCann
schedule
21.06.2010