Добро пожаловать в мир ленивой оценки.
Когда вы думаете об этом с точки зрения строгой оценки, foldl выглядит «хорошо», а foldr выглядит «плохо», потому что foldl является хвостом рекурсивным, но foldr должен будет построить башню в стеке, чтобы обработать последний элемент первый.
Однако ленивая оценка превращает таблицы. Возьмем, например, определение функции карты:
map :: (a -> b) -> [a] -> [b]
map _ [] = []
map f (x:xs) = f x : map f xs
Это не было бы слишком хорошо, если 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 слишком ленив, чтобы выполнить дополнения, как она идет. Вместо этого он заканчивается башней необоснованных громов, которые должны быть вынуждены получить номер. Во время этой оценки происходит переполнение стека, так как оно должно глубоко заново оценивать все трюки.
К счастью, в Data.List есть специальная функция, называемая foldl'
, которая работает строго. foldl' (+) 0 [1..1000000]
не будет переполнять переполнение. (Примечание: Я попытался заменить foldl
с foldl'
в тесте, но он на самом деле сделал это работать медленнее.)
Кстати, я бы использовал конструктор списка, чтобы написать 'a' как' a = (:) ' –
да. Единственная причина, по которой я это сделал ([x] ++), заключалась в том, чтобы попытаться сделать a и b как можно ближе, чтобы сравнить складывание так близко, как я мог, – Ori
... и 'b' как' b = flip (:) '. –