函数如何调用自身

时间:2019-03-12 19:08:15

标签: recursion lisp computer-science environment

我了解递归,但我不知道这是不可能的。我将使用休闲示例进一步说明我的问题。

(def (pow (x, y))
     (cond ((y = 0) 1))
           (x * (pow (x , y-1))))

以上程序为Lisp语言。我不确定语法是否正确,因为我脑中想出了它,但是它可以。在程序中,我定义了pow函数,并在pow中对其进行了调用。我不知道该怎么做。据我所知,计算机必须先对功能进行全面分析,然后才能对其进行定义。如果是这种情况,那么当我使用pow时,计算机应该给出未定义的消息,因为我在定义pow之前就使用了它。我要描述的原理是在x = x + 1中使用x时起作用的原理,而x之前未定义。

3 个答案:

答案 0 :(得分:2)

编译器比您想象的要聪明得多。 编译器可以在此定义中打开递归调用:

(defun pow (x y)
  (cond ((zerop y) 1)
        (t (* x (pow x (1- y))))))

插入goto指令以从头开始重新启动功能:

Disassembly of function POW
(CONST 0) = 1
2 required arguments
0 optional arguments
No rest parameter
No keyword parameters
12 byte-code instructions:
0     L0
0     (LOAD&PUSH 1)
1     (CALLS2&JMPIF 172 L15)              ; ZEROP
4     (LOAD&PUSH 2)
5     (LOAD&PUSH 3)
6     (LOAD&DEC&PUSH 3)
8     (JSR&PUSH L0)
10    (CALLSR 2 57)                       ; *
13    (SKIP&RET 3)
15    L15
15    (CONST 0)                           ; 1
16    (SKIP&RET 3)

如果这是一个更复杂的递归函数,编译器无法将其展开到循环中,则它将仅再次调用该函数。

答案 1 :(得分:1)

函数只是代码块。名称只是帮助,因此您不必计算其最终地址。编程语言会将名称转换为要执行程序的位置。

一个函数如何调用另一个函数是通过在该函数的堆栈上存储下一个命令的地址,也许在堆栈中添加参数,然后跳转到该函数的地址位置。函数本身跳转到找到的返回地址,以便控制权返回给被调用者。该语言在哪一侧执行什么操作,有几种调用约定。 CPU实际上并没有功能支持,就像在CPU功能中没有所谓的while循环一样。

就像函数具有名称一样,参数也具有名称,但是它们只是指针,就像返回地址一样。调用自身时,它只是将新的返回地址和参数添加到堆栈上并跳转到自身。堆栈的顶部将不同,因此相同的变量名称是该调用的唯一地址,因此上一次调用中的xy与当前x和{{ 1}}。实际上,调用自己不需要调用其他任何东西。

从历史上看,第一高级语言Fortran不支持递归。它会自行调用,但返回时返回到原始被调用者,而在自调用之后不执行其余功能。如果没有递归,Fortran本身将无法编写,因此尽管其本身使用了递归,但并未将其提供给使用它的程序员。这种限制是约翰·麦卡锡(John McCarthy)发现Lisp的原因。

答案 2 :(得分:0)

我想看看它通常如何工作,特别是在递归调用不能变成循环的情况下,值得思考一下通用的编译语言可能会起作用,因为问题没有什么不同。

让我们想象一下编译器如何将此功能转换为机器代码:

(defun foo (x)
  (+ x (bar x)))

让我们假设它在编译时对bar一无所知。好吧,它有两个选择。

  1. 它可以通过编译foo的方式来编译对bar的调用,转换为一组指令,即“查找以名称bar存储的函数定义,无论目前是这样,并安排使用正确的参数调用该函数。
  2. 它可以编译foo的方式,使得有一个对函数的机器级函数调用,但是该函数的地址保留为某种占位符。然后,它可以将一些元数据附加到foo上,其中说:“在调用此函数之前,您需要找到名为bar的函数,找到其地址,并在正确的位置将其拼接到代码中,< em>并删除此元数据。

这两种机制都允许在知道foo是什么之前定义bar。请注意,我可以写bar而不是foo:这些机制也处理递归调用。但是,它们与此不同。

  • 第一种机制意味着,每次调用foo时,都需要对bar进行某种动态查找,这会涉及一些开销(但是开销可能很小):
    • 因此,第一种机制会比可能慢一些;
    • 但是,因此,如果bar被重新定义,那么新的定义将被接受,这对于Lisp实现通常是交互式语言来说是非常可取的。
  • 第二种机制意味着,在foo链接了所有对其他函数的引用之后,这些调用将在计算机级别发生:
    • 这意味着他们会很快;
    • 但是,重新定义充其量只会更加复杂,或者最糟糕的是根本不可能。

第二种实现方式与传统的编译器如何编译代码非常接近:它们编译代码时会留下一堆占位符以及相关的元数据,这些元数据说明了这些占位符对应的名称。然后, linker (有时称为链接加载器或加载器)会在编译器生成的所有文件以及其他代码库中进行搜索,并解析所有这些引用,从而导致可以实际运行的代码。

一个思维简单的Lisp系统可能完全可以通过第一种机制工作(例如,我很确定这就是Python的工作方式)。一个更高级的编译器可能会通过第一种和第二种机制的某种组合来工作。例如,CL允许编译器假设函数中的明显自调用实际上是 自调用,因此编译器很可能将它们编译为直接调用(本质上它将编译功能,然后将其动态关联)。但是,一般而言,在编译代码时,它可能会调用“通过函数名称”。

事情也可以做一些或多或少的英勇策略:例如,在函数的第一次调用时,将它动态链接到它所引用的所有事物,并在他们的< / em>定义,如果它们发生更改,则也需要取消链接,以便再次发生。这些技巧曾经看起来是难以置信的,但是像JavaScript这样的语言的编译器现在所做的事情至少一直如此。


请注意,由于共享库&c,现代系统的编译器和链接器实际上所做的事情比我所描述的还要复杂:我所描述的或多或少是发生在预共享库中的。