Начните с простой задачи, например, сопоставления элементов из списка «a» с «b» в списке. Мы хотим написать функцию, которая имеет подпись
val map: ('a -> 'b) -> 'a list -> 'b list
Где
map (fun x -> x * 2) [1;2;3;4;5] == [2;4;6;8;10]
Начните с не хвостовой рекурсии версия:
let rec map f = function
| [] -> []
| x::xs -> f x::map f xs
Это не хвостовая рекурсия, потому что функция все еще должна работать после выполнения рекурсивного вызова. ::
является синтаксическим сахаром для List.Cons(f x, map f xs)
.
Нерекурсивный характер функции мог бы быть немного более очевидным, если бы я переписал последнюю строку как | x::xs -> let temp = map f xs; f x::temp
- очевидно, она выполняет свою работу после рекурсивного вызова.
Используйте переменную аккумулятора , чтобы сделать его хвостовым рекурсивным:
let map f l =
let rec loop acc = function
| [] -> List.rev acc
| x::xs -> loop (f x::acc) xs
loop [] l
Вот мы создаем новый список в переменной acc
. Поскольку список создается в обратном порядке, нам нужно обратить вспять список вывода, прежде чем возвращать его пользователю.
Если вы хотите немного изменить сознание, вы можете использовать продолжение продолжения , чтобы написать код более кратко:
let map f l =
let rec loop cont = function
| [] -> cont []
| x::xs -> loop ( fun acc -> cont (f x::acc) ) xs
loop id l
Поскольку вызовы loop
и cont
являются последними функциями, вызываемыми без дополнительной работы, они рекурсивны.
Это работает, потому что продолжение cont
перехватывается новым продолжением, которое, в свою очередь, перехватывается другим, что приводит к некоторой древовидной структуре данных следующим образом:
(fun acc -> (f 1)::acc)
((fun acc -> (f 2)::acc)
((fun acc -> (f 3)::acc)
((fun acc -> (f 4)::acc)
((fun acc -> (f 5)::acc)
(id [])))))
, который формирует список по порядку, не требуя его переворачивания.
Для чего стоит начинать писать функции нехвостым рекурсивным способом, с ними легче читать и работать.
Если вам нужен большой список, используйте переменную-аккумулятор.
Если вы не можете найти способ использовать аккумулятор удобным способом и у вас нет других возможностей, используйте продолжения. Лично я считаю, что нетривиальное, интенсивное использование продолжений трудно читать.