Racket 和 Common Lisp 中顶级函数定义顺序的规则

Rules for top-level function definitions order in Racket and Common Lisp

我发现顶级声明顺序似乎并不重要。是否有关于该主题的文档?不是很懂

示例显示无需定义即可调用函数

#lang racket

(define (function-defined-early)
  (function-defined-later))

(define (function-defined-later)
  1)

(function-defined-early)
> 1
;; Common Lisp

(defun function-defined-early ()
  (function-defined-later))

(defun function-defined-later ()
  1)

(print (function-defined-early))
> 1

对于 Common Lisp,它有点复杂,因为实现可以使用解释代码、编译代码和高度优化的编译代码。

简单编译代码中的函数调用

例如,SBCL 默认编译所有代码。甚至是通过 read-eval-print-loop 输入的代码:

* (defun foo (a) (bar (1+ a)))
; in: DEFUN FOO
;     (BAR (1+ A))
;
; caught STYLE-WARNING:
;   undefined function: COMMON-LISP-USER::BAR
;
; compilation unit finished
;   Undefined function:
;     BAR
;   caught 1 STYLE-WARNING condition
FOO

由于函数被立即编译,编译器发现有一个未定义的函数。但这只是警告而不是错误。生成的代码将调用函数 bar,即使它是稍后定义的。

符号有函数值

在 Common Lisp 中,全局函数的函数对象被注册为符号。

* (fboundp 'foo)
T
* (fboundp 'bar)
NIL

bar 没有函数定义。如果我们稍后为 bar 定义一个函数,那么我们之前定义的函数 foo 的代码将调用这个新函数。

它是如何工作的? foo 中的代码在运行时进行查找以获取符号 bar 的函数值并调用该函数。

因此我们也可以重新定义barfoo会调用新的函数。

后期绑定

执行函数运行时查找的概念通常称为后期绑定。这是 1960 年代对 Lisp 的描述。

因此调用了一个全局函数

(bar 1 a)

在概念上与

基本相同
(if (fbound 'bar)
    (funcall (symbol-function 'bar) 1 a)
    (error "undefined function BAR"))

请记住,这是一个简单的模型,实际上 Common Lisp 文件编译器 可能会使用更积极的优化(如内联),其中没有运行时查找。

函数形式的评价

Common Lisp 标准在 Conses as Forms 中说:

If the operator is neither a special operator nor a macro name, it is assumed to be a function name (even if there is no definition for such a function).

就 Common Lisp 而言,如果您尝试加载一个引用未知函数的顶层表单(例如在 SLIME 中:C-c C-c),您通常会收到警告。

但是,load使用多个顶层表单 loading 一个文件(例如在 SLIME 中:C-c C-k)首先加载所有这些表单,然后才(通常)检查丢失的引用。在任何情况下,在编译或加载时缺少引用都不是错误。

这有点简化,但 CLHS 章节非常通用,可以适应非常不同的实现,并且(在我看来)提供的指导很少。然而,以上是一个基本的期望——在单个文件中不需要前向声明

除了 Scheme 和 CL 的特定语义(至少对于 CL 而言,它们相当复杂并且允许以各种方式变化),我认为您对何时调用函数感到困惑。我将考虑 CL 示例并假设一个完全天真的程序正在按顺序评估您给出的定义。像这样的程序:

(defun naively-evaluate-file (f)
  (let ((*package* *package*))
    (with-open-file (in f)
      (loop for form = (read in nil in)
            until (eql form in)
            collect (eval form)))))

那么,好的,这个函数在浏览您的文件时做了什么?

  1. 它看到表单 (defun function-defined-early () (function-defined-later)) 并对其进行评估。评估 defun 形式定义了一个函数,function-defined-early 并且该函数 在被调用时 将调用尚未定义的 function-defined-later。但是没有调用函数,所以没有问题。
  2. 它看到 (defun function-defined-later () 1) 并对其进行评估,它定义(但不调用)function-defined-later
  3. 它看到 (print (function-defined-early)),它调用 function-defined-early,因此调用 function-defined-later,两者都已定义,并打印结果。

所以你可以看到,其实函数在定义之前并没有被调用。函数在它们调用的函数被定义之前被定义,但这些函数在定义时不会被调用。


顺便说一句,如果要在语言中允许递归,那么在定义函数时,这种前向引用定义几乎是不可避免的。考虑阶乘函数的这个糟糕定义:

(defun fact (n)
  (if (= n 1)
      1
    (* n (fact (1- n)))))

好吧,当系统评估此定义以定义 fact 时,它会看到对 fact 的调用...尚未定义。好吧,也许您可​​以将其作为特殊情况(并且允许 CL 编译器这样做):假设该调用实际上是对您正在定义的函数的调用。

因此您可以使用仅递归调用 自身 的特殊函数。但是一旦你有两个或更多函数递归调用彼此,你就无法避免其中一个,在它被定义(未调用!)的地方,指的是一些尚未定义的功能。所以定义时的前向引用问题几乎是不可避免的。

(好吧,事实上你可以避免它:你可以用 Y 组合器或其他东西做你所有的递归,但是虽然这在理论上很有趣(并且为家庭作业问题提供难以理解的答案),但没有人愿意在实践中做到这一点。)

Common Lisp 本质上是一个汇编程序。这是非常有活力的。考虑 CLISP 中的以下交互:

[1]> (defun foo (x) (defun bar (y) y) x)
FOO
[2]> (bar 4)

*** - EVAL: undefined function BAR
The following restarts are available:
USE-VALUE      :R1      You may input a value to be used instead of (FDEFINITION
 'BAR).
RETRY          :R2      Retry
STORE-VALUE    :R3      You may input a new value for (FDEFINITION 'BAR).
ABORT          :R4      ABORT
Break 1 [3]> :r4
[4]> (foo 3)
3
[5]> (bar 4)
4
[6]> (defun foo (x) (bar (+ 1 x)))
FOO
[7]> (foo 3)
4
[8]> (defun bar (x) (+ 2 x))
BAR
[9]> (foo 3)
6
[10]>

它只是采用函数调用时生效的函数的最新定义并使用that。正如我们刚刚看到的,您可以自由地重新定义您的函数,如果从其他函数(一般)引用,新版本将被调用。

Scheme / Racket 是完全不同的东西。它的内心是静止的。通过使用 environments 解析任何函数引用。如果您在嵌套环境中重新定义您的函数,"later"(如果它甚至被允许这样做),如果它被引用,原始版本仍然会被调用。

Racket源文件的顶级函数都属于同一个环境。尝试加载源文件实际上是错误的,其中某些函数被定义为调用另一个实际上未在同一范围内定义的函数,在源文件中进一步向下某处 (如果不在某些库中).

Common Lisp 很乐意加载这样的文件,因为用户总是可以在以后通过任何方式定义缺失的函数——而且有很多——供他们使用.