为什么一折尾递归而另一折不是?

时间:2018-10-09 08:52:06

标签: sml tail-recursion smlnj

fun fold1 f acc lst =
    case lst of
         [] => acc
       | hd::tl => fold1 f (f (acc,hd)) tl

fun fold2 f acc lst =
    case lst of
         [] => acc
       | hd::tl => f (fold2 f acc tl, hd)

为什么第一个是尾递归的而另一个不是?

我认为它们都是尾递归的。

3 个答案:

答案 0 :(得分:3)

第一个是尾递归,因为(对fold1的递归调用出现在函数体的末尾(形成“尾巴”):

fold1 f (f (acc,hd)) tl

首先调用f (acc,hd),然后将结果(以及ftl)传递到fold1。这是函数要做的最后一件事。递归fold1调用的结果将传递给我们的调用者。


第二个不是尾递归,因为(对foldl2的递归调用不是该函数执行的最后操作:

f (fold2 f acc tl, hd)

首先调用fold2 f acc tl,然后根据结果创建一个元组,然后hd,然后将该元组传递给f

在递归fold2调用之后发生了两件事:元组构造((..., hd))和另一个函数调用(f ...)。特别是,调用fold2的结果不会直接传递给我们的调用者。这就是为什么这段代码不是尾递归的。

答案 1 :(得分:0)

  

为什么第一个是尾递归的而另一个不是?

给出一个definition of tail-recursive

  

如果函数返回后除返回其值外无事可做,则称该函数调用为尾递归。

在第一个功能中,

fun fold1 f acc lst =
    case lst of
         [] => acc
       | hd::tl => fold1 f (f (acc,hd)) tl

所有其他计算(f (acc, hd))作为fold1的参数执行,这意味着该函数返回后除返回其值外没有其他操作

在第二个功能中,

fun fold2 f acc lst =
    case lst of
         [] => acc
       | hd::tl => f (fold2 f acc tl, hd)

所有其他计算(f (..., hd))是在执行完 fold2 f acc tl之后执行的,这意味着在该函数之后需要执行 返回,即计算f (..., hd)

尾递归函数具有定义特征,即其递归函数主体的最外层表达式是对自身的调用。如果函数中计算出其他任何内容,则应在调用之前 发生,例如作为函数参数或在let绑定中。

例如,fold1的以下重构也是尾递归的:

fun fold1 f acc0 [] = acc0
  | fold1 f acc0 (x::xs) =
    let val acc1 = f (acc0, x)
    in fold1 f acc1 xs
    end

以及以下fold2的重构不是:

fun fold2 f acc0 [] = acc0
  | fold2 f acc0 (x::xs) =
    let val acc1 = fold2 f acc0 xs
    in f (acc1, x)
    end

由于fold1 f acc1 xs之后没有其他事情要做,因此可以安全地丢弃函数调用的上下文(堆栈框架)。非尾递归与尾递归的一个简单示例是:

fun length [] = 0
  | length (_::xs) = 1 + length xs

fun length xs =
    let fun go [] count = count
          | go (_::ys) count = go ys (1 + count)
    in go xs 0
    end

答案 2 :(得分:0)

TL; DR

如果是尾部递归,则必须位于尾部位置。参数不是尾巴位置。


您的情况:

case lst of
     [] => e1
   | hd::tl => e2

e1e2都是尾写形式,因为在评估两个表达式的 value 后没有其他表达式 (IOW,它们位于尾部位置)。这就是fold1是尾递归的原因。

对于f (fold2 f acc tl, hd)f(...)确实位于尾巴位置。但是fold2不在尾部,因为在评估其之后,您仍然需要调用f。因此f是尾递归,而fold2不是尾递归。