Clojure 为什么TCO需要VM的支持?

Clojure 为什么TCO需要VM的支持?,clojure,lisp,tail-recursion,tail-call-optimization,Clojure,Lisp,Tail Recursion,Tail Call Optimization,有些虚拟机,尤其是JVM,据说不支持TCO。因此,像Clojure这样的语言要求用户使用循环重现 但是,我可以重写self-tail调用以使用循环。例如,这里有一个尾部调用阶乘: def factorial(x, accum): if x == 1: return accum else: return factorial(x - 1, accum * x) 这里有一个循环等价物: def factorial(x, accum): whil

有些虚拟机,尤其是JVM,据说不支持TCO。因此,像Clojure这样的语言要求用户使用
循环
重现

但是,我可以重写self-tail调用以使用循环。例如,这里有一个尾部调用阶乘:

def factorial(x, accum):
    if x == 1:
        return accum
    else:
        return factorial(x - 1, accum * x)
这里有一个循环等价物:

def factorial(x, accum):
    while True:
        if x == 1:
            return accum
        else:
            x = x - 1
            accum = accum * x
这可以由编译器来完成(我已经编写了实现这一点的宏)。对于相互递归,您可以简单地内联正在调用的函数

那么,既然您可以实现TCO而不需要任何VM,为什么语言(例如Clojure、Armed Bear Common Lisp)不这样做呢?我错过了什么

  • 这可能会令人惊讶,并且可能会使调试更加困难,因为您无法看到调用堆栈

  • 它只适用于非常特殊的情况,而不是在VM支持TCO时处理的一般情况

  • 程序员通常不会递归地编写代码尾部,除非语言鼓励他们这样做。例如,递归阶乘通常使用递归步骤编写为
    n*fact(n-1)
    ,这不是尾部递归


  • 内联不是一般尾部调用消除问题的解决方案,原因有很多。下面的列表并非详尽无遗。然而,它是被分类的——它以一个不便开始,以一个完整的停止结束

  • 在尾部位置调用的函数可能是一个较大的函数,在这种情况下,从性能角度来看,将其内联可能是不可取的

  • 假设在
    f
    中有多个对
    g
    的尾部调用。根据内联的常规定义,您必须在每个调用站点内联
    g
    ,这可能会使
    f
    变得巨大。相反,如果您选择
    转到
    g
    的开头,然后跳回,那么您需要记住跳转到哪里,并且突然之间您正在维护自己的调用堆栈片段(与“真正的”调用堆栈相比,这几乎肯定会表现出较差的性能)

  • 对于相互递归的函数
    f
    g
    ,必须在
    g
    中内联
    f
    ,在
    f
    中内联
    g
    。显然,根据内联的通常定义,这是不可能的。因此,您只剩下一个有效的自定义函数调用约定(如上面2.中基于
    goto
    的方法)

  • Real TCE在尾部位置处理任意调用,包括在高阶上下文中:

    (defn say-foo-then-call [f]
      (println "Foo!")
      (f))
    
    这可以在某些场景中发挥巨大作用,显然不能用内联来模拟

  • TCO本身不需要VM支持。也就是说,不适用于本地功能。跨越外部函数的尾部调用需要VM支持。理想情况下,尾部递归的完整实现允许单独编译的程序单元中的函数在常量空间中相互递归,而不仅仅是一个父函数的局部函数,或者编译器一次可以看到所有函数

    在不支持尾部调用的VM中,函数调用被封装,并在退出时分配新的帧。尾部调用需要一个特殊的入口点来绕过该入口点。函数可以参与尾部调用,也可以参与非尾部调用,因此它们需要两个入口点


    尾部调用消除可以在没有VM支持的情况下使用非本地返回和分派进行模拟。也就是说,当尾部调用在语法上发生时,它被转换为非本地返回,该返回通过动态控制传递放弃当前函数,将参数(可能打包为对象)传递给隐藏的分派循环,该分派循环将控制传递给目标函数,并将这些参数传递给它。这将实现递归在恒定空间中发生的要求,并将“外观和感觉”类似于尾部调用。但是,它很慢,而且可能不是完全透明的。

    请注意,TCO并不是专门针对自递归调用的。这只是一个特例,没错。但是要么调用同一个函数(可以作为循环编写),要么调用另一个函数(可以内联)。我相信这两种技术结合在一起是一个通用的解决方案。内联不是解决方案。可以简单地在Clojure中内联函数调用吗?我看到的例子是将一个(希望是相应的)宏作为元数据附加到
    defn
    。您所说的是尾部递归的最简单形式。现在考虑尾部调用虚拟方法。您不一定知道要调用哪个函数。哪些情况不包括重写为循环和内联?你能举个例子吗?嗯,你通常不希望所有的函数调用都内联。答案很好,我没有考虑过很多问题。谢谢:)