Слева и справа Складывание по бесконечному списку
У меня есть проблемы со следующим отрывком из Learn You A Haskell (Великая книга imo, а не раскаяние):
Одно большое различие заключается в том, что право складки работают над бесконечными списками, в то время как левые - нет! Положить если вы возьмете бесконечный список в какой-то момент, и вы сложите его справа, вы в конечном итоге достигнете начала списка. Однако, если вы берете бесконечный список в точке и пытаетесь свернуть это слева, вы никогда не достигнете конца!
Я просто этого не понимаю. Если вы берете бесконечный список и пытаетесь сложить его справа, вам придется начинать с бесконечной точки, чего просто не происходит (если кто-то знает язык, на котором вы можете это сделать, скажите: p). По крайней мере, вам нужно будет начать работу в соответствии с реализацией Haskell, потому что в Haskell foldr и foldl не принимается аргумент, определяющий, где в списке они должны начать складывать.
Я бы согласился с цитатой iff foldr и foldl принял аргументы, которые определили, где в списке они должны начать складывать, потому что имеет смысл, что если вы берете бесконечный список и начинаете складываться прямо из определенного индекса, он в конечном итоге прекратится, в то время как неважно, где вы начинаете с левой складки; вы будете складываться в бесконечность. Однако foldr и foldl не принимают этот аргумент, и, следовательно, цитата не имеет смысла. В Haskell, как левая справка, так и правая сфера над бесконечным списком не заканчиваются.
Насколько я понимаю, я что-то упускаю?
Ответы
Ответ 1
Ключ здесь - лень. Если функция, которую вы используете для свертывания списка, является строгой, то ни левая сгиб, ни правая складка не будут завершены, учитывая бесконечный список.
Prelude> foldr (+) 0 [1..]
^CInterrupted.
Однако, если вы попытаетесь свернуть менее строгую функцию, вы можете получить завершающий результат.
Prelude> foldr (\x y -> x) 0 [1..]
1
Вы даже можете получить результат, который представляет собой бесконечную структуру данных, поэтому, хотя он в некотором смысле не заканчивается, он все еще способен произвести результат, который можно лениво потреблять.
Prelude> take 10 $ foldr (:) [] [1..]
[1,2,3,4,5,6,7,8,9,10]
Однако это не будет работать с foldl
, так как вы никогда не сможете оценить внешний вызов функции, ленивы или нет.
Prelude> foldl (flip (:)) [] [1..]
^CInterrupted.
Prelude> foldl (\x y -> y) 0 [1..]
^CInterrupted.
Обратите внимание, что различие клавиш между левой и правой складками - это не тот порядок, в котором перемещается список, который всегда слева направо, а скорее как вложенные приложения функций вложены.
-
С foldr
они вложены в "внутри"
foldr f y (x:xs) = f x (foldr f y xs)
Здесь первая итерация приведет к самому внешнему приложению f
. Таким образом, f
имеет возможность быть ленивым, так что второй аргумент либо не всегда оценивается, либо может создавать некоторую часть структуры данных, не вызывая его второй аргумент.
-
С foldl
они вложены в "наружную сторону"
foldl f y (x:xs) = foldl f (f y x) xs
Здесь мы не можем ничего оценивать, пока не достигнем самого внешнего приложения f
, которое мы никогда не достигнем в случае бесконечного списка, независимо от того, является ли f
строгим или нет.
Ответ 2
Ключевая фраза - "в какой-то момент".
если вы берете бесконечный список в какой-то точке, и вы складываете его справа, вы в конечном итоге достигнете начала списка.
Итак, вы правы, вы не можете начинать с "последнего" элемента бесконечного списка. Но автор указывает на это: предположим, вы могли бы. Просто выберите точку waaay далеко там (для инженеров, это "достаточно близко" до бесконечности) и начните складывать влево. В конце концов вы попадаете в начало списка. То же самое не относится к левой складке, если вы выберете точку waaaay (и назовите ее "достаточно близко" к началу списка) и начните складывать вправо, у вас все еще есть бесконечный путь.
Итак, трюк, иногда вам не нужно идти в бесконечность. Вам, возможно, не нужно даже идти waaaay там. Но вы можете не знать, как далеко вы должны пройти заранее, и в этом случае бесконечные списки весьма удобны.
Простая иллюстрация foldr (:) [] [1..]
. Позвольте выполнить сгиб.
Напомним, что foldr f z (x:xs) = f x (foldr f z xs)
. В бесконечном списке фактически не имеет значения, что z
, поэтому я просто сохраняю его как z
вместо []
, который загромождает иллюстрацию
foldr (:) z (1:[2..]) ==> (:) 1 (foldr (:) z [2..])
1 : foldr (:) z (2:[3..]) ==> 1 : (:) 2 (foldr (:) z [3..])
1 : 2 : foldr (:) z (3:[4..]) ==> 1 : 2 : (:) 3 (foldr (:) z [4..])
1 : 2 : 3 : ( lazily evaluated thunk - foldr (:) z [4..] )
Посмотрите, как foldr
, несмотря на то, что теоретически это сгиб справа, в этом случае фактически выкатывает отдельные элементы результирующего списка, начиная слева? Поэтому, если вы take 3
из этого списка, вы можете ясно видеть, что он сможет создать [1,2,3]
и не должен оценивать сгиб еще дальше.
Ответ 3
Помните, что в Haskell вы можете использовать бесконечные списки из-за ленивой оценки. Итак, head [1..]
равно 1, а head $ map (+1) [1..]
равно 2, хотя `[1..] бесконечно длинный. Если вы этого не сделаете, остановитесь и поиграйте с ним некоторое время. Если вы это поняли, прочитайте...
Я думаю, что часть вашего замешательства состоит в том, что foldl
и foldr
всегда начинаются с одной или другой стороны, поэтому вам не нужно указывать длину.
foldr
имеет очень простое определение
foldr _ z [] = z
foldr f z (x:xs) = f x $ foldr f z xs
почему это может закончиться бесконечными списками, попробуйте
dumbFunc :: a -> b -> String
dumbFunc _ _ = "always returns the same string"
testFold = foldr dumbFunc 0 [1..]
здесь мы переходим в foldr
a "" (поскольку значение не имеет значения) и бесконечный список натуральных чисел. Это прекращается? Да.
Причина, по которой он заканчивается, заключается в том, что оценка Haskell эквивалентна ленивому переписыванию терминов.
So
testFold = foldr dumbFunc "" [1..]
становится (чтобы разрешить сопоставление с образцом)
testFold = foldr dumbFunc "" (1:[2..])
что совпадает с (из нашего определения складки)
testFold = dumbFunc 1 $ foldr dumbFunc "" [2..]
теперь по определению dumbFunc
можно заключить
testFold = "always returns the same string"
Это более интересно, когда у нас есть функции, которые что-то делают, но иногда ленивы. Например
foldr (||) False
используется, чтобы определить, содержит ли список какие-либо элементы True
. Мы можем использовать это для определения функции более высокого порядка n any
, которая возвращает True
тогда и только тогда, когда переданная функция истинна для некоторого элемента списка
any :: (a -> Bool) -> [a] -> Bool
any f = (foldr (||) False) . (map f)
Хорошая вещь о ленивой оценке - это то, что она остановится, когда встретит первый элемент e
такой, что f e == True
С другой стороны, это не относится к foldl
. Зачем? Ну, действительно простой foldl
выглядит как
foldl f z [] = z
foldl f z (x:xs) = foldl f (f z x) xs
Теперь, что бы случилось, если мы попробовали наш пример выше
testFold' = foldl dumbFunc "" [1..]
testFold' = foldl dumbFunc "" (1:[2..])
теперь это становится:
testFold' = foldl dumbFunc (dumbFunc "" 1) [2..]
так
testFold' = foldl dumbFunc (dumbFunc (dumbFunc "" 1) 2) [3..]
testFold' = foldl dumbFunc (dumbFunc (dumbFunc (dumbFunc "" 1) 2) 3) [4..]
testFold' = foldl dumbFunc (dumbFunc (dumbFunc (dumbFunc (dumbFunc "" 1) 2) 3) 4) [5..]
и т.д. и т.д. Мы никогда не сможем получить никуда, потому что Haskell всегда сначала оценивает внешнюю функцию (это ленивая оценка в двух словах).
Одно из последствий этого - вы можете реализовать foldl
из foldr
, но не наоборот. Это означает, что в некотором глубоком ключе foldr
является самой фундаментальной из всех строковых функций более высокого порядка, так как это тот, который мы используем для реализации почти всех остальных. Вы по-прежнему можете иногда использовать foldl
, потому что вы можете рекурсивно реализовать хвост foldl
и получить от этого прирост производительности.
Ответ 4
Существует хорошее объяснение Haskell wiki. Он показывает поэтапное сокращение с помощью различных типов функций сгиба и аккумулятора.
Ответ 5
Ваше понимание верное. Интересно, пытается ли автор говорить о ленивой системе оценки Haskell (в которой вы можете передавать бесконечный список для различных функций, не считая сгибов, и он будет оценивать только то, что необходимо для ответа). но я согласен с вами в том, что автор не делает хорошую работу, описывая что-либо в этом параграфе, и то, что он говорит, неверно.