Добро пожаловать в мир ленивых оценок.
Когда вы думаете об этом с точки зрения строгой оценки, foldl выглядит «хорошо», а foldr выглядит «плохо», потому что foldl является рекурсивным хвостом, но foldr придется построить башню в стеке, чтобы он мог обработать последний элемент первым .
Однако, ленивая оценка переворачивает таблицы. Взять, к примеру, определение функции карты:
map :: (a -> b) -> [a] -> [b]
map _ [] = []
map f (x:xs) = f x : map f xs
Это не было бы слишком хорошо, если бы Haskell использовал строгую оценку, так как он должен был бы сначала вычислить хвост, а затем предварительно добавить элемент (для всех элементов в списке). Кажется, что единственный способ сделать это эффективно - построить элементы в обратном порядке.
Однако, благодаря ленивой оценке Haskell, эта функция карты фактически эффективна. Списки в Haskell можно рассматривать как генераторы, и эта функция карты генерирует свой первый элемент, применяя f к первому элементу входного списка. Когда ему нужен второй предмет, он просто делает то же самое снова (без использования дополнительного места).
Оказывается, что map
можно описать с помощью foldr
:
map f xs = foldr (\x ys -> f x : ys) [] xs
Трудно сказать, глядя на это, но ленивая оценка срабатывает, потому что foldr может сразу дать f
свой первый аргумент:
foldr f z [] = z
foldr f z (x:xs) = f x (foldr f z xs)
Поскольку f
, определяемый map
, может возвращать первый элемент списка результатов, используя только первый параметр, сгиб может лениво работать в постоянном пространстве.
Теперь ленивая оценка кусается назад. Например, попробуйте запустить сумму [1..1000000]. Это приводит к переполнению стека. Зачем это? Стоит только оценить слева направо, верно?
Давайте посмотрим, как Haskell оценивает это:
foldl f z [] = z
foldl f z (x:xs) = foldl f (f z x) xs
sum = foldl (+) 0
sum [1..1000000] = foldl (+) 0 [1..1000000]
= foldl (+) ((+) 0 1) [2..1000000]
= foldl (+) ((+) ((+) 0 1) 2) [3..1000000]
= foldl (+) ((+) ((+) ((+) 0 1) 2) 3) [4..1000000]
...
= (+) ((+) ((+) (...) 999999) 1000000)
Haskell слишком ленив для выполнения дополнений. Вместо этого это заканчивается башней неоцененных громов, которые должны быть вынуждены получить число. Переполнение стека происходит во время этой оценки, так как он должен глубоко повторяться для оценки всех thunks.
К счастью, в Data.List есть специальная функция foldl'
, которая работает строго. foldl' (+) 0 [1..1000000]
не будет переполняться стеком. (Примечание: я пытался заменить foldl
на foldl'
в вашем тесте, но на самом деле он замедлился.)