如何检测可以应用尾调用优化的函数?

时间:2013-09-15 20:21:23

标签: lisp scheme tail-call-optimization

我正在尝试编写一个Scheme解释器,但发现TCO很难实现。我不确定函数必须具有哪些属性才能使TCO启动。

1)在定义结尾处具有自递归调用的函数:

(define (sum-list list accum)
  (if (null list) accum
      (sum-list (cdr list) (+ accum 1))))

2)具有自递归调用的函数,该函数不在定义的末尾:

(define (sum-list list accum)
  (cond ((not (null list))
         (sum-list (cdr list) (+ accum 1)))
        (else accum)))

3)在返回变量之前将递归调用存储在变量中的函数:

(define (sum-list list accum)
  (let ((sum
         (if (null list)
             accum
             (sum-list (cdr list (+ accum 1))))))
    sum))

4)相互递归函数:

(define (sum-list list accum)
  (if (null list)
      accum
      (other-function (cdr list) (+ accum 1))))

(define (other-function list accum)
  (sum-list list accum))

5)在函数定义的末尾简单地调用另一个不相关的函数:

(define (foo)
  (bar))

6)我能想到的最棘手的案例是封闭。如果我继续引用范围内的变量怎么办?

(define (sum-list list accum ignored)
  (let ((local-var 12345))
    (if (null list)
        accum
        (sum-list
         (cdr list)
         (+ accum 1)
         (lambda () local-var)))))

7)在函数定义的末尾调用另一个函数,并以自递归调用作为参数:

(define (sum-list list)
  (if (null list)
      0
      (+ 1 (sum-list (cdr list)))))

据我了解,TCO实现(在Scheme和Common Lisp中)都重写了TCO友好函数,因此它们必须能够静态检测TCO友好函数。

我想知道的是:

  • TCO会改写哪些功能,为什么只有那些呢?
  • 是否发生TCO重写的情况(例如7),但该函数仍会线性消耗内存?

2 个答案:

答案 0 :(得分:7)

看看Scheme规范,那里定义了所有可能的尾部上下文。特别是在R6RS(当前批准的标准)中,您应该查看§11.20 Tail calls and tail contexts部分:

  

尾调用是在尾部上下文中发生的过程调用。尾部上下文是归纳的。请注意,尾部上下文始终根据特定的lambda表达式确定。

     
      
  • lambda表达式正文中的最后一个表达式,显示为< tail expression>下面,发生在尾部上下文中。

    (lambda <formals>
      <definition>* 
      <expression>* <tail expression>)
    
  •   
  • 如果以下表达式之一位于尾部上下文中,则子表达式显示为&lt; tail expression&gt;处于尾巴背景下。这些是通过替换某些&lt; expression&gt;的出现来源自本章所述形式语法的规范。与&lt;尾部表达&gt;。这里仅显示包含尾部上下文的那些规则。

    (if <expression> <tail expression> <tail expression>)
    (if <expression> <tail expression>)
    
    (cond <cond clause>+)
    (cond <cond clause>* (else <tail sequence>))
    
    (case <expression>
      <case clause>+)
    (case <expression>
      <case clause>*
      (else <tail sequence>))
    
    (and <expression>* <tail expression>)
    (or <expression>* <tail expression>)
    
    (let <bindings> <tail body>)
    (let <variable> <bindings> <tail body>)
    (let* <bindings> <tail body>)
    (letrec* <bindings> <tail body>)
    (letrec <bindings> <tail body>)
    (let-values <mv-bindings> <tail body>)
    (let*-values <mv-bindings> <tail body>)
    
    (let-syntax <bindings> <tail body>)
    (letrec-syntax <bindings> <tail body>)
    
    (begin <tail sequence>)
    
         

    A&lt; cond子句&gt;是(<test> <tail sequence>),&lt; case子句&gt;是((<datum>*) <tail sequence>),&lt;尾体&gt;是<definition>* <tail sequence>,&lt;尾序列&gt;是<expression>* <tail expression>

  •   
  • 如果cond表达式位于尾部上下文中,并且具有(<expression1> => <expression2>)形式的子句,那么(隐含)调用由&lt;表达式求值得出的过程<子> 2 &GT;处于尾巴背景下。 &LT;表达<子> 2 &GT;本身不是尾巴背景。

  •   

答案 1 :(得分:7)

你还没有提到这将解释Scheme的运行时或语言,所以可能这个答案有点偏。

实现尾部呼叫优化并同时获得呼叫/ cc的万无一失的方法是进行CPS转换。使用CPS,每个调用都是一个尾调用,而非尾递归代码则是嵌套的延续。

在应用尾调用之前弹出堆栈时会得到TCO。由于每次通话都是CPS的尾部通话,因此您知道该怎么做。

除了3和7之外,你的每个程序都会从​​中受益。当递归调用返回时,这些程序还有继续。

编辑Python:

Python没有TCO,因此您无法使用主机语言直接进行调用。但是,你可以do it with trampolines