Как сделать хвостовую рекурсивную функцию - PullRequest
4 голосов
/ 25 сентября 2019

Я действительно запутался в том, как сделать функцию "Хвостовой рекурсией".

Вот моя функция, но я не знаю, является ли она хвостовой рекурсивной или нет.

Я пытаюсь объединить два списка в Haskell.

merge2 :: Ord a =>[a]->[a]->[a]
merge2 xs [] = xs
merge2 [] ys = ys
merge2 (x:xs)(y:ys) = if y < x then y: merge2 (x:xs) ys else x :merge2 xs (y:ys)

1 Ответ

4 голосов
/ 25 сентября 2019

Ваша функция не является хвостовой рекурсией;он охраняется рекурсивноОднако защищенная рекурсия - это то, что вам следует использовать в Haskell, если вы хотите эффективно использовать память.

Чтобы вызов был хвостовым, его результат должен быть результатом всей функции.Это определение применяется как к рекурсивным, так и к нерекурсивным вызовам.

Например, в коде

f x y z = (x ++ y) ++ z

вызов (x ++ y) ++ z является хвостовым вызовом, поскольку его результат является результатом всей функции.Вызов x ++ y является , а не хвостовым вызовом.

В качестве примера хвостовой рекурсии рассмотрим foldl:

foldl :: (b -> a -> b) -> b -> [a] -> b
foldl _ acc [] = acc
foldl f acc (x:xs) = foldl f (f acc x) xs

Рекурсивный вызов foldl f (f acc x) xs является хвостово-рекурсивным вызовом, потому что его результат является результатом всей функции.

Рекурсивные вызовы в вашем коде

merge2 (x:xs)(y:ys) = if y < x then y: merge2 (x:xs) ys else x :merge2 xs (y:ys)

не являются хвостово-рекурсивными, потому что они не даютрезультат всей функции.После вызова merge2 вы берете результат и используете его для создания нового списка.Конструктор (:), а не рекурсивный вызов, дает результат всей функции.

Однако хвостовая рекурсия не гарантирует эффективность пространства в ленивом языке.При ленивой оценке Haskell создает thunks или структуры в памяти, которые представляют код, который еще предстоит оценить.Рассмотрим оценку следующего кода:

foldl f 0 (1:2:3:[])
=> foldl f (f 0 1) (2:3:[])
=> foldl f (f (f 0 1) 2) (3:[])
=> foldl f (f (f (f 0 1) 2) 3) []
=> f (f (f 0 1) 2) 3

Вы можете считать ленивую оценку происходящей «вовне».Когда рекурсивные вызовы foldl оценены, в аккумуляторе накапливаются громоотводы.Таким образом, хвостовая рекурсия с аккумуляторами неэффективна с точки зрения пространства в ленивом языке из-за отложенной оценки.

Вместо хвостовой рекурсии следует пытаться использовать защищенную рекурсию, где рекурсивный вызов скрыт внутри конструктора данных.При ленивой оценке выражения оцениваются до тех пор, пока они не будут в нормальной форме слабой головы (WHNF).Выражение находится в WHNF, если оно либо:

  • Конструктор данных, примененный к аргументам (например, Just (1 + 1))
  • Частично примененная функция (например, const 2)
  • Лямбда-выражение (например, \x -> x)

Рассмотрим map:

map :: (a -> b) -> [a] -> [b]
map _ [] = []
map f (x:xs) = f x : map f xs

map (+1) (1:2:3:[])
=> (+1) 1 : map (+1) (2:3:[])

Выражение (+1) 1 : map (+1) (2:3:[]) в WHNF из-за данных (:)конструктор, и, следовательно, оценка останавливается на этом этапе.Ваша функция merge2 также использует защищенную рекурсию, поэтому она также экономит место на ленивом языке.

TL; DR: на ленивом языке хвостовая рекурсия все еще может занимать память, если она формирует thunksв аккумуляторе, пока защищенная рекурсия не накапливает гром.

Полезные ссылки:

...