Почему моя интуиция о самореференциальных ленивых последовательностях неверна?

В Haskell я могу написать самореляционную последовательность в GHCI, например:

λ> let x = 1:map (+1) x
λ> take 5 x

который производит:

[1,2,3,4,5]

Однако моя интуиция относительно ленивой оценки говорит, что это должно произойти во время расширения

let x = 1:map (+1) x
1:2:map (+1) x
1:2:map (+1) [1, 2] <-- substitution
1:2:2:3:map (+1) x
1:2:2:3:map (+1) [1, 2, 2, 3] <-- substitution
1:2:2:3:2:3:3:4:map (+1) x
...

Это явно не то, что происходит. Я вижу шаблон в правильном ответе. Мы просто перемещаем один элемент в списке за раз в бесконечный поток. Я признаю шаблон, и я могу применить его в коде. Однако это не соответствует моей ментальной модели ленивой оценки. Он чувствует себя немного "волшебным". Где моя интуиция ошибается?

Ответы

Ответ 1

Помните только, чтобы заменить что-то своим определением. Поэтому всякий раз, когда вы расширяете x, вы должны подставить 1 : map (+1) x, а не его "текущее значение" (что бы это ни значило).

Я воспроизведу идею Джефффри, но с должным уважением к лени.

x = 1 : map (+1) x

take 5 x
= take 5 (1 : map (+1) x)                                 -- x
= 1 : take 4 (map (+1) x)                                 -- take
= 1 : take 4 (map (+1) (1 : map (+1) x)                   -- x
= 1 : take 4 (2 : map (+1) (map (+1) x))                  -- map and (+)
= 1 : 2 : take 3 (map (+1) (map (+1) x))                  -- take
= 1 : 2 : take 3 (map (+1) (map (+1) (1 : map (+1) x)))   -- x
= 1 : 2 : take 3 (map (+1) (2 : map (+1) (map (+1) x)))   -- map and (+)
= 1 : 2 : take 3 (3 : map (+1) (map (+1) (map (+1) x)))   -- map and (+)
= 1 : 2 : 3 : take 2 (map (+1) (map (+1) (map (+1) x)))   -- take

и т.д.

Упражнение завершает оценку в этом стиле самостоятельно (это довольно информативно).

Обратите внимание, как мы начинаем наращивать цепочку map по мере роста списка. Если вы просто print x, через некоторое время вы увидите, что начало запуска замедляется; вот почему. Существует более эффективный способ, оставленный как упражнение (и [1..] изменяет: -).

N.B. это все еще немного менее лениво, чем то, что на самом деле произойдет. map (+1) (1 : ...) оценивается как (1+1) : map (+1) ..., и добавление произойдет только тогда, когда число действительно наблюдается, либо путем его печати, либо, например, сравнивая его.

Уилл Несс указал на ошибку в этом сообщении; см. комментарии и его ответ.

Ответ 2

Вот что происходит. Лень - это нестрогость + мемонирование (thunks). Мы можем показать это, назвав все временные данные, которые возникают, когда выражение принудительно:

λ> let x  = 1  : map (+1) x
   >>> x  = a1 : x1                             -- naming the subexpressions
       a1 = 1
       x1 = map (+1) x 

λ> take 5 x 
==> take 5 (a1:x1)                              -- definition of x
==> a1:take 4 x1                                -- definition of take
          >>> x1 = map (1+) (1:x1)              -- definition of x
                 = (1+) 1 : map (1+) x1         -- definition of map
                 = a2     : x2                  -- naming the subexpressions
              a2 = (1+) 1                        
              x2 = map (1+) x1  
==> a1:take 4 (a2:x2)                           -- definition of x1
==> a1:a2:take 3 x2                             -- definition of take
             >>> x2 = map (1+) (a2:x2)          -- definition of x1
                    = (1+) a2 : map (1+) x2     -- definition of map
                    = a3      : x3              -- naming the subexpressions
                 a3 = (1+) a2                    
                 x3 = map (1+) x2  
==> a1:a2:take 3 (a3:x3)                        -- definition of x2
==> a1:a2:a3:take 2 x3                          -- definition of take
                >>> x3 = map (1+) (a3:x3)       -- definition of x2
.....

Элементы в результирующем потоке a1:a2:a3:a4:... относятся к своему предшественнику: a1 = 1; a2 = (1+) a1; a3 = (1+) a2; a4 = (1+) a3; ....

Таким образом, это эквивалентно x = iterate (1+) 1. Без совместного использования данных и их повторного использования через обратную ссылку (включенную с помощью memoization storage) она будет эквивалентна x = [sum $ replicate n 1 | n <- [1..]], которая является радикально менее эффективным вычислением (O (n 2)) вместо O (n)).

Мы можем объяснить совместное использование без разделения с помощью

fix g = x where x = g x        -- sharing fixpoint
x = fix ((1:) . map (1+))      --  corecursive definition

_Y g = g (_Y g)                -- non-sharing fixpoint
y = _Y ((1:) . map (1+))       --  recursive definition

Попытка распечатать y в приглашении GHCi показывает заметное замедление по мере продвижения. Там нет замедления при печати потока x.

(см. также fooobar.com/questions/5304/... для аналогичного примера).

Ответ 3

Вы сопоставляете +1 по всему списку, так что начальный 1 становится n, где n - это количество раз, когда вы лениво рекурсируете, если это имеет смысл. Поэтому вместо вывода, о котором вы думаете, это выглядит более похоже:

1:...                            -- [1 ...]
1: map (+1) (1:...)              -- [1, 2 ...]
1: map (+1) (1:map (+1) (1:...)) -- [1, 2, 3 ...]

A 1 добавляется к лениво вычисляемому списку, чьи элементы все увеличиваются на каждом этапе рекурсии.

Итак, вы можете подумать о n -ом шаге рекурсии, взяв список [1, 2, 3, ..., n ...], превратив его в список [2, 3, 4, ..., n+1 ...] и добавив 1.

Ответ 4

Давайте посмотрим на это немного математически. Предположим, что

x = [1, 2, 3, 4, ...]

Тогда

map (+1) x = [2, 3, 4, 5, ...]

так

1 : map (+1) x = 1 : [2, 3, 4, 5, ...] = x

Это (повернутое) - это уравнение, с которого мы начали:

x = 1 : map (+1) x

Итак, мы показали, что

x = [1, 2, 3, 4, ...]

- решение уравнения

x = 1 : map (+1) x   -- Eqn 1

Следующий вопрос, конечно, заключается в том, есть ли какие-либо другие решения для уравнения 1. Ответ, как оказалось, - нет. Это важно, потому что модель оценки Haskell эффективно выбирает "наименее определенное" решение любого такого уравнения. Например, если бы мы определили x = 1 : tail x, то любой список, начинающийся с 1, был бы решением, но мы фактически получили бы 1 : _|_, где _|_ представляет ошибку или не прерывание. Уравнение 1 не приводит к такому беспорядку:

Пусть y - любое решение уравнения 1, поэтому

y = 1 : map (+1) y

Заметим, что из определения можно сказать, что

take 1 y = [1] = take 1 x

Предположим теперь, что

take n y = take n x

Тогда

take (n+1) y = take (n+1) (1 : map (+1) y)
             = 1 : take n (map (+1) y)
             = 1 : map (+1) (take n y)
             = 1 : map (+1) (take n x)
             = 1 : take n (map (+1) x)
             = take (n+1) (1 : map (+1) x)
             = take (n+1) x

По индукции получаем, что take n y = take n x для каждого n. То есть y = x.

Ответ 5

Что вы ошибаетесь

В вашем порядке оценки:

let x = 1:map (+1) x
1:2:map (+1) x
1:2:map (+1) [1, 2] <-- here

вы делаете неправильное предположение. Вы принимаете x is [1, 2], потому что количество элементов, которые вы можете увидеть там. Не это. Вы забыли подумать, что x в конце нужно вычислить рекурсивно.

Фактический поток

x в конце последовательности должен быть вычислен путем рекурсивного вычисления самого себя. Здесь фактический поток:

take 5 $ 1:map (+1) ...
take 5 $ 1:map (+1) (1:map (+1) ...
take 5 $ 1:map (+1) (1:map (+1) (1:map (+1) ...
take 5 $ 1:map (+1) (1:map (+1) (1:map (+1) (1:map (+1) ...
take 5 $ 1:map (+1) (1:map (+1) (1:map (+1) (1:map (+1) (1:map (+1) ...
take 5 $ 1:map (+1) (1:map (+1) (1:map (+1) (1:map (+1) [1 ...
take 5 $ 1:map (+1) (1:map (+1) (1:map (+1) [1, 2 ...
take 5 $ 1:map (+1) (1:map (+1) [1, 2, 3 ...
take 5 $ 1:map (+1) [1, 2, 3, 4 ...
take 5 $ [1, 2, 3, 4, 5 ...
[1, 2, 3, 4, 5]