Сворачивание влево и вправо по бесконечному списку - PullRequest
69 голосов
/ 13 сентября 2011

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

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

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

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

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

Ответы [ 5 ]

85 голосов
/ 13 сентября 2011

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

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

17 голосов
/ 13 сентября 2011

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

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

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

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

Простая иллюстрация - 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] и не нужно оценивать сгиб дальше.

12 голосов
/ 13 сентября 2011

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

0 голосов
/ 10 апреля 2015

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

0 голосов
/ 13 сентября 2011

Ваше понимание верно. Интересно, пытается ли автор говорить о ленивой системе оценки Хаскелла (в которой вы можете передать бесконечный список различным функциям, не считая фолд, и он будет только оценивать, сколько нужно для возврата ответа). но я согласен с вами, что автор не очень хорошо описывает что-либо в этом абзаце и что он говорит неправильно.

...