it-swarm.com.ru

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

У меня есть проблемы со следующим отрывком из Learn You A Haskell (Отличная книга, я не оспариваю ее):

Одно большое отличие - это право. складки работают с бесконечными списками, а левые - нет! Чтобы поставить это просто если в какой-то момент вы берете бесконечный список и складываете его справа вы в конце концов дойдете до начала списка . Однако, если вы берете бесконечный список в какой-то момент и пытаетесь сбросить это слева, ты никогда не достигнешь конца!

Я просто не понимаю этого. Если вы берете бесконечный список и пытаетесь свернуть его справа, то вам придется начинать с бесконечной точки, чего просто не происходит (если кто-нибудь знает язык, на котором вы можете это сделать, скажите: p ). По крайней мере, вам придется начать там в соответствии с реализацией Haskell, потому что в Haskell foldr и foldl не принимают аргумент, который определяет, где в списке они должны начать складываться.

Я бы согласился с цитатой, если бы foldr и foldl принимали аргументы, которые определяли, где в списке они должны начинать сворачивание, потому что имеет смысл, что если вы возьмете бесконечный список и начнете сворачивать прямо из определенного индекса, это will в конечном итоге завершается, в то время как не имеет значения, где вы начинаете с левой складки; вы будете сворачиваться в бесконечность. Однако foldr и foldl не принимают этот аргумент, и, следовательно, цитата не имеет смысла. В Haskell и левый, и правый сгибы над бесконечным списком не заканчиваются.

Правильно ли мое понимание или я что-то упустил?

69
TheIronKnuckle

Ключ здесь - лень. Если функция, которую вы используете для свертывания списка, является строгой, то ни левый, ни правый сгибы не прекратятся, учитывая бесконечный список.

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 строгим или нет.

84
hammar

Ключевая фраза «в какой-то момент».

если вы возьмете бесконечный список в какой-то момент и сложите его справа, вы в конечном итоге достигнете начала списка.

Таким образом, вы правы, вы не можете начинать с «последнего» элемента бесконечного списка. Но авторская точка зрения такова: предположим, вы могли бы. Просто выберите точку, где-то далеко (для инженеров это «достаточно близко» к бесконечности) и начинайте складывать влево. В конце концов вы в конечном итоге в начале списка. То же самое не относится к левому сгибу, если вы выберете точку «ваааайа» (и назовете ее «достаточно близко» к началу списка) и начнете сворачивание вправо, у вас все равно будет бесконечный путь.

Итак, хитрость в том, что иногда вам не нужно уходить в бесконечность. Вам, возможно, не нужно даже идти ваааай там. Но вы можете не знать, как далеко вам нужно идти заранее, и в этом случае бесконечные списки весьма удобны.

Простая иллюстрация 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, несмотря на то, что теоретически является сгибом от right, в этом случае фактически выдает отдельные элементы результирующего списка, начиная с left? Таким образом, если вы take 3 из этого списка, вы можете ясно видеть, что он сможет генерировать [1,2,3], и вам не нужно оценивать сгиб еще дальше.

17
Dan Burton

Помните, что в 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 "" (так как значение не имеет значения) и бесконечный список натуральных чисел. Это заканчивается? Да. 

Причина, по которой он завершается, заключается в том, что оценка Хаскелла эквивалентна ленивому переписыванию термина. 

Так 

 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 тогда и только тогда, когда переданная функция имеет значение 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 и получить от этого некоторое повышение производительности. 

11
Philip JF

На Haskell wiki есть хорошее простое объяснение. Показывает пошаговое сокращение с различными типами сгиба и функциями накопителя.

0
xuesheng