F# 我的rec函数是递归的吗?

F# 我的rec函数是递归的吗?,f#,recursion,tail-recursion,F#,Recursion,Tail Recursion,这个函数是递归的吗 let rec rec_algo1 step J = if step = dSs then J else let a = Array.init (Array2D.length1 M) (fun i -> minby1J i M J) let argmin = a|> Array.minBy snd |> fst rec_algo1 (step+1) (argmin::J) 一般来说,是否有办


这个函数是递归的吗

let rec rec_algo1 step J = 
    if step = dSs then J
    else
        let a = Array.init (Array2D.length1 M) (fun i -> minby1J i M J)
        let argmin = a|> Array.minBy snd |> fst
        rec_algo1 (step+1) (argmin::J)
一般来说,是否有办法正式检查它


谢谢。

此函数是尾部递归函数;我一眼就能看出来


总的来说,这并不总是容易说出来的。也许最可靠/最实用的方法就是在一个大的输入上检查它(并确保您是在“发布”模式下编译的,因为“调试”模式会关闭尾部调用以进行更好的调试)。

是的,您可以正式证明函数是尾部递归的。每个表达式归约都有一个尾部位置,如果所有递归都在尾部位置,则函数是尾部递归的。一个函数在一个地方可能是尾部递归的,但在另一个地方则不可能

在表达式
中,让pat=exprA在exprB
中仅
exprB
处于尾部位置。也就是说,虽然您可以去评估
exprA
,但您仍然需要回来评估
exprB
,并记住
exprA
。对于语言中的每个表达式,都有一个缩减规则,告诉您尾部位置在哪里。在
ExprA中;ExprB
又是
ExprB
。在
if ExprA然后ExprB else ExprC
中,它既是
ExprB
又是
ExprC
,依此类推

编译器当然知道这一点。然而,F#中有许多可用的表达式,以及编译器在编译过程中执行的许多内部优化,例如在模式匹配编译期间,
seq{}
async{}
等计算表达式可以使知道哪些表达式处于尾部位置变得不明显


实际上,通过一些实践,小函数很容易通过查看嵌套表达式并检查不在函数调用尾部位置的插槽来确定尾部调用。(请记住,尾部调用可能是对另一个函数的!)

您问我们如何正式检查这一点,我想试试。我们首先必须定义函数尾部递归的含义。形式的递归函数定义

let rec f x_1 ... x_n = e
如果
e
内部的
f
的所有调用都是尾部调用,则为尾部递归调用-即发生在尾部上下文中。尾部上下文
C
被归纳地定义为带有孔的术语
[]

C ::= []
    | e
    | let p = e in C
    | e; C
    | match e with p_1 -> C | ... | p_n -> C
    | if e then C else C
其中,
e
是一个F#表达式,
x
是一个变量,
p
是一个模式。我们应该将其扩展到相互递归的函数定义,但我将把它作为练习

现在,让我们将此应用于您的示例。函数体中对
rec_algo1
的唯一调用是在以下上下文中:

if step = dSs then J
else
    let a = Array.init (Array2D.length1 M) (fun i -> minby1J i M J)
    let argmin = a|> Array.minBy snd |> fst
    []

因为这是一个尾部上下文,所以函数是尾部递归的。这就是函数式程序员观察它的方式——扫描定义体中的递归调用,然后验证每个调用都发生在尾部上下文中。尾部调用的一个更直观的定义是,除了返回调用结果外,对调用结果不做任何其他操作。

您可以检查编译器是否识别它(毕竟,这才是最重要的),但必须向编译器询问相应分析过程的结果(或者自己实现一些,希望它们不比编译器更聪明)。这可能回答您的第二个问题,也可能不回答您的第二个问题,但是,如果您的函数在递归调用后没有执行任何操作,则它是尾部递归的。有一个迹象表明它不是尾部递归的:您正在对递归调用的返回值执行某些操作,而不是返回它。另请参阅,谢谢Brian。您对编译模式的评论也很有帮助。@Br伊恩-你能通过检查功能的相应IL来判断吗?同时,一般的正式检查可能很困难(不可能?),你能举出一些例子来说明哪些地方仅仅通过目测是很难判断的吗?我见过很多次它说,它并不总是很容易判断的,但我从来没有见过一个足够复杂的尾部递归函数不容易判断。在某个地方,我看到有人提到F#团队正在考虑一个未来的关键字,可以让你判断他告诉编译器,您希望函数是尾部递归的,因此编译器可以在实际不是尾部递归的情况下警告您。我认为这会非常方便。FWIW,
tailcall
是一个保留字。@Stephen-有两件事很容易被忽略:(1)仔细查看调用,但忘记您正在使用
try
(例如,
在函数顶部使用
是否否定尾部调用?我想是这样)和(2)在执行相互递归时,尾部调用尾部调用的函数参数(特别是当某些参数可能部分应用或可能不部分应用时).我手头没有示例,但根据经验我知道,如果你相信你的眼球,你会偶尔感到惊讶。更罕见的情况是跨部分信任程序集边界的相互递归(从不尾部调用)。