Пример рекурсивной функции хвоста F #

Я новичок в F # и читал о хвостовых рекурсивных функциях и надеялся, что кто-то может дать мне две различные реализации функции foo - ту, которая является хвостом рекурсивной, и одна, которая не настолько, чтобы я мог лучше понять принцип.

Ответ 1

Начните с простой задачи, например, сопоставления элементов с '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 [])))))

который создает список в порядке, не требуя от него изменения.


Для чего стоит, начинайте писать функции нерекурсивным способом, их легче читать и работать.

Если у вас есть большой список для перехода, используйте переменную аккумулятора.

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

Ответ 2

Попытка более короткого объяснения, чем в других примерах:

let rec foo n =
    match n with
    | 0 -> 0
    | _ -> 2 + foo (n-1)

let rec bar acc n =
    match n with
    | 0 -> acc
    | _ -> bar (acc+2) (n-1)

foo не является хвостовым рекурсивным, так как foo должен вызывать foo рекурсивно, чтобы оценить "2 + foo (n-1)" и вернуть его.

bar является хвостом рекурсивным, потому что для возврата значения бар не нужно использовать возвращаемое значение рекурсивного вызова. Он может просто позволить рекурсивно называемому бару возвращать свое значение сразу (не возвращаясь полностью, хотя вызывающий стек). Компилятор видит это и "обманывает", переписывая рекурсию в цикл.

Изменение последней строки в строке на "| _ → 2+ (bar (acc + 2) (n-1))" уничтожит рекурсивность хвоста.

Ответ 3

Вот более очевидный пример, сравните его с тем, что вы обычно делаете для факториала.

let factorial n =
    let rec fact n acc =
        match n with
        | 0 -> acc
        | _ -> fact (n-1) (acc*n)
    fact n 1

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

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

Ответ 4

Я тоже изучаю F #. Ниже приведены нерекурсивные и хвостовые рекурсивные функции для вычисления чисел фибоначчи.

Рекурсивная версия без хвоста

let rec fib = function
    | n when n < 2 -> 1
    | n -> fib(n-1) + fib(n-2);;

Рекурсивная версия хвоста

let fib n =
    let rec tfib n1 n2 = function
    | 0 -> n1
    | n -> tfib n2 (n2 + n1) (n - 1)
    tfib 0 1 n;;  

Примечание: поскольку число фибаначчей может расти очень быстро, вы можете заменить последнюю строку tfib 0 1 n на
tfib 0I 1I n, чтобы воспользоваться структурой Numerics.BigInteger в F #

Ответ 5

Кроме того, при тестировании не забывайте, что косвенная хвостовая рекурсия (tailcall) отключается по умолчанию при компиляции в режиме отладки. Это может привести к тому, что рекурсия tailcall переполняет стек в режиме Debug, но не в режиме Release.