Как узнать, является ли функция хвостовой рекурсивной в F#
Я написал следующую функцию:
let str2lst str =
let rec f s acc =
match s with
| "" -> acc
| _ -> f (s.Substring 1) (s.[0]::acc)
f str []
Как я могу узнать, что компилятор F# превратил его в цикл? Есть ли способ узнать без использования Reflector (у меня нет опыта работы с Reflector, и я не знаю C#)?
Редактировать: Кроме того, возможно ли написать хвостовую рекурсивную функцию без использования внутренней функции, или это необходимо для цикла, чтобы находиться в?
Кроме того, есть ли в F# std lib функция для запуска данной функции несколько раз, каждый раз давая ей последний вывод в качестве входных данных? Допустим, у меня есть строка, я хочу запустить функцию над строкой, затем снова запустить ее над результирующей строкой и так далее...
3 ответа
К сожалению, нет тривиального пути.
Нетрудно прочитать исходный код и использовать типы и определить, является ли что-то хвостовым вызовом путем проверки (это "последнее", а не в блоке "try"), но люди сами догадываются и делать ошибки. Нет простого автоматизированного способа (кроме, например, проверки сгенерированного кода).
Конечно, вы можете просто попробовать свою функцию на большом фрагменте тестовых данных и посмотреть, взорвется он или нет.
Компилятор F# будет генерировать инструкции.tail IL для всех оконечных вызовов (если не используются флаги компилятора для их отключения - используется, когда вы хотите сохранить стековые фреймы для отладки), за исключением того, что непосредственно хвостовые рекурсивные функции будут оптимизированы в петли. (РЕДАКТИРОВАТЬ: я думаю, что в настоящее время компилятор F# также не может генерировать.tail в тех случаях, когда он может доказать, что на этом сайте вызовов нет рекурсивных циклов; это оптимизация, учитывая, что код операции.tail немного медленнее на многих платформах.)
tailcall - зарезервированное ключевое слово, с мыслью, что будущая версия F# может позволить вам написать, например:
tailcall func args
и затем получите предупреждение / ошибку, если это не хвостовой вызов.
Только функции, которые не имеют естественной хвостовой рекурсии (и, следовательно, нуждаются в дополнительном параметре аккумулятора), "заставят" вас использовать идиому "внутренней функции".
Вот пример кода того, что вы спросили:
let rec nTimes n f x =
if n = 0 then
x
else
nTimes (n-1) f (f x)
let r = nTimes 3 (fun s -> s ^ " is a rose") "A rose"
printfn "%s" r
Компилятор может проверить это за вас, начиная с F# 8! Отметьте функцию знакомTailCallAttribute
, и вы получите предупреждение, если предоставите реализацию, не являющуюся хвостовой рекурсией.
Например, этот код:
[<TailCall>]
let rec fibNaive n =
if n <= 1 then n
else fibNaive (n - 1) + fibNaive (n - 2)
выдает два экземпляраWarning FS3569 : The member or function 'fibNaive' has the 'TailCallAttribute' attribute, but is not being used in a tail recursive way.
(по одному для каждого рекурсивного вызова)
Хотя этот код не выдает предупреждений:
[<TailCall>]
let fibFast n =
let rec fibFast x y n =
match n with
| _ when n < 1 -> x
| 1 -> y
| _ -> fibFast y (x + y) (n - 1)
fibFast 0 1 n
Для получения бонусных баллов было бы неплохо превратить это в ошибку, добавив<WarningsAsErrors>FS3569</WarningsAsErrors>
в ваш fsproj.
Мне нравится эмпирическое правило, которое Пол Грэхем формулирует в " На Лиспе": если осталось сделать работу, например, манипулировать выводом рекурсивного вызова, то вызов не является хвостовым рекурсивным.