Слева и справа Складывание по бесконечному списку

У меня есть проблемы со следующим отрывком из 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 (в которой вы можете передавать бесконечный список для различных функций, не считая сгибов, и он будет оценивать только то, что необходимо для ответа). но я согласен с вами в том, что автор не делает хорошую работу, описывая что-либо в этом параграфе, и то, что он говорит, неверно.