Как обрабатывается совместная рекурсия?
Хорошо, в основном у меня проблема с пониманием того, применима ли опция 1 или 2 в следующем случае:
naturals = 0 : map (+ 1) naturals
Где варианты:
1. Выполнение ужасно, все пересчитывается на каждом шаге:
naturals = [0]
naturals' = 0:map (+ 1) [0] // == [0, 1]
naturals'' = 0:map (+ 1) [0, 1] // == [0, 1, 2]
naturals''' = 0:map (+ 1) [0, 1, 2] // == [0, 1, 2, 3]
naturals'''' = 0:map (+ 1) [0, 1, 2, 3] // == [0, 1, 2, 3, 4]
2. Выполнение не ужасно, список всегда бесконечен и map
применяется только один раз
naturals = 0:something
|
naturals' = 0: map (+ 1) (0: something)
|
naturals'' = 0:1: map (+ 1) (0:1: something')
|
naturals''' = 0:1:2: map (+ 1) (0:1:2: something'')
|
naturals'''' = 0:1:2:3:map (+ 1) (0:1:2:3:something''')
с |
, указывающим, где map
находится в его выполнении.
Я знаю, что ответы могут быть только 1 или 2, но я был бы признателен за некоторые пояснения к хорошим объяснениям по совместной рекурсии, чтобы устранить последние сомнения:)
Ответы
Ответ 1
Исполнение не будет, как вы выразились, "ужасным".:) Ленькая оценка - ваш лучший друг здесь. Что означает лень?
- Вещи не оцениваются до того, как их результаты действительно понадобятся;
- Вещи оцениваются не чаще одного раза.
"Вещи", здесь, являются "еще не оцененными выражениями", также известными как "thunks".
Вот что происходит:
Вы определяете
naturals = 0 : map (+1) naturals
Простое определение naturals
не представляет необходимости его оценивать, поэтому изначально naturals
будет просто указывать на thunk для неоцениваемого выражения 0 : map (+1) naturals
:
naturals = <thunk0>
В какой-то момент ваша программа может иметь совпадение с рисунком по naturals. (Совмещение шаблонов - это, по сути, единственное, что заставляет оценивать в программе Haskell.) То есть ваша программа должна знать, являются ли naturals пустым списком или элементом head, за которым следует хвостовой список. Здесь будет оценена правая часть вашего определения, но только по мере необходимости, чтобы выяснить, построена ли naturals
с помощью []
или (:)
:
naturals = 0 : <thunk1>
Это naturals теперь укажет на приложение конструктора (:)
на элемент head 0
и thunk для все еще неоцененного хвоста. (На самом деле элемент head также будет по-прежнему не оценен, так что действительно naturals
укажет на что-то из формы <thunk> : <thunk>
, но я оставлю эту деталь.)
Только в какой-то более поздней точке вашей программы, где вы можете сопоставлять совпадение хвоста, чтобы thunk для хвоста "принудительно", т.е. оценивался. Это означает, что выражение map (+1) naturals
должно быть оценено. Оценка этого выражения сводится к map
сопоставлению шаблонов на naturals
: ему нужно знать, построена ли naturals
с помощью []
или (:)
.
Мы видели, что на данный момент вместо того, чтобы указывать на thunk, naturals
уже указывает на приложение (:)
, поэтому соответствие шаблона map
не требует дальнейшей оценки. Приложение map
сразу видит достаточно naturals
, чтобы понять, что ему нужно создать приложение самого (:)
, и поэтому оно: map
создает 1 : <thunk2>
, где thunk содержит неоценимое выражение форма map (+1) <?>
. (Опять же, вместо 1
, на самом деле у нас есть thunk для 0 + 1
.) Что указывает <?>
? Ну, хвост naturals
, который, как оказалось, производит map
. Следовательно, теперь имеем
naturals = 0 : 1 : <thunk2>
с <thunk2>
, содержащим еще не оцененное выражение map (+1) (1 : <thunk2>)
.
В еще более поздней точке вашей программы совпадение шаблонов может привести к <thunk2>
, так что мы получим
naturals = 0 : 1 : 2 : <thunk3>
с <thunk3>
, содержащим еще не оцененное выражение map (+1) (2 : <thunk3>)
. И так далее.
Ответ 2
Мне потребовалось некоторое время, чтобы понять это, но если вы хотите найти (скажем) миллиардное натуральное число,
n = nats !! 1000000000
вы нажмете накопление thunk в операции 1+. Я закончил переписывание (!!):
nth (x:xs) n = if n==0 then x else x `seq` nth xs (n-1)
Я попробовал несколько способов переписать определение nats, чтобы заставить каждый элемент вместо написания nth, но ничего не работало.
Ответ 3
map f xs = f (head xs) : map f (tail xs)
p0 = 0 : map (+ 1) p0
-- when p0 is pattern-matched against:
p0 = "0" :Cons: "map (+ 1) {p0}"
-- when (tail p0) is pattern-matched against:
-- {tail p0} := p1,
p1 = "(+ 1) (head {p0})" :Cons: "map (+ 1) (tail {p0})"
-- when (tail p1) is pattern-matched against:
-- {tail p1} := p2,
p2 = "(+ 1) (head {p1})" :Cons: "map (+ 1) (tail {p1})"
Списки Haskell очень похожи на списки открытых списков Prolog, а со-рекурсия по спискам - как рекурсия хвоста по модулю минусов. После того, как вы создадите экземпляр этого логарифма - задайте значение ячейки списка из некоторого выражения - он просто сохраняет это значение, больше нет ссылки на исходный контекст.
naturals( [A|T] ):- T=[B|R], B=A+1, naturals( T ). % "=" sic! ("thunk build-up")
Чтобы преодолеть строгость Prolog, мы делаем следующий доступ к процессу:
naturals( nats(0) ).
next( nats(A), A, nats(B) ):-
B is A+1. % fix the evaluation to be done immediately
take( 0, Next, Z-Z, Next).
take( N, Next, [A|B]-Z, NZ):- N>0, !, next(Next,A,Next1),
N1 is N-1,
take(N1,Next1,B-Z,NZ).
Haskell позаботился об этом легко, его "хранилище" естественно лениво (т.е. конструктор списка ленив, а построение списка только "просвещено" путем доступа по самой природе языка).
Fix
Сравните эти:
fix f = f (fix f)
fix f = x where x = f x -- "co-recursive" fix ?
Теперь посмотрите, как ваша первоначальная проблема становится реальной, когда первое определение используется в следующем:
g = fix $ (0:) . scanl (+) 1
Его эмпирическая сложность фактически квадратична или хуже. Но со вторым определением оно линейно, как и должно быть.