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

Racket和Common Lisp中顶级函数定义顺序的规则,lisp,common-lisp,racket,Lisp,Common Lisp,Racket,我发现顶级声明顺序似乎并不重要。有关于这个主题的文档吗?我不太明白 示例显示无需定义即可调用函数 #lang racket (define (function-defined-early) (function-defined-later)) (define (function-defined-later) 1) (function-defined-early) > 1 对于普通的Lisp,它有点复杂,因为实现可以使用解释代码、编译代码和高度优化的编译代码 简单编译代码中的函

我发现顶级声明顺序似乎并不重要。有关于这个主题的文档吗?我不太明白

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

#lang racket

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

(define (function-defined-later)
  1)

(function-defined-early)
> 1

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

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

例如,默认情况下,SBCL编译所有代码。即使是通过读取-评估打印循环输入的代码:

* (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
由于该函数立即被编译,编译器会看到有一个未定义的函数。但这只是一个警告,而不是一个错误。生成的代码将调用函数栏,即使它是稍后定义的

符号具有函数值

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

* (fboundp 'foo)
T
* (fboundp 'bar)
NIL
bar没有函数定义。如果我们后来为bar定义了一个函数,那么前面定义的函数foo的代码将调用这个新函数

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

因此,我们还可以重新定义bar,foo将调用新函数

后期装订

执行函数运行时查找的概念通常称为后期绑定。这是在20世纪60年代为Lisp所描述的

因此,需要调用全局函数

(bar 1 a)
在概念上与

(if (fbound 'bar)
    (funcall (symbol-function 'bar) 1 a)
    (error "undefined function BAR"))
请记住,这是一个过于简单的模型,在现实中,一个普通的Lisp文件编译器可能会使用更积极的优化,比如内联,在内联中没有运行时查找

函数形式的评价

通用Lisp标准在中说明:

如果运算符既不是特殊运算符,也不是宏名,则假定它是函数名,即使这种函数没有定义


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

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

例如,默认情况下,SBCL编译所有代码。即使是通过读取-评估打印循环输入的代码:

* (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
由于该函数立即被编译,编译器会看到有一个未定义的函数。但这只是一个警告,而不是一个错误。生成的代码将调用函数栏,即使它是稍后定义的

符号具有函数值

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

* (fboundp 'foo)
T
* (fboundp 'bar)
NIL
bar没有函数定义。如果我们后来为bar定义了一个函数,那么前面定义的函数foo的代码将调用这个新函数

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

因此,我们还可以重新定义bar,foo将调用新函数

后期装订

执行函数运行时查找的概念通常称为后期绑定。这是在20世纪60年代为Lisp所描述的

因此,需要调用全局函数

(bar 1 a)
在概念上与

(if (fbound 'bar)
    (funcall (symbol-function 'bar) 1 a)
    (error "undefined function BAR"))
请记住,这是一个过于简单的模型,在现实中,一个普通的Lisp文件编译器可能会使用更积极的优化,比如内联,在内联中没有运行时查找

函数形式的评价

通用Lisp标准在中说明:

如果运算符既不是特殊运算符,也不是宏名,则假定它是函数名,即使这种函数没有定义


对于CommonLisp,如果您尝试加载一个顶级表单e。G在SLIME:C-C-C中,引用了一个未知的未定义函数,通常会得到一个警告

但是,加载文件e。G在SLIME中:具有多个顶级表单的C-C-k首先加载所有表单,然后才检查缺少的引用。在任何情况下,编译或加载时缺少引用都不是错误


这有点简化,但是CLHS章节非常通用,可以容纳非常不同的实现,在我看来,它提供的指导很少。但是,以上是一个基本要求,不需要在单个文件中进行转发声明。

对于Common Lisp,如果您尝试加载单个顶级表单e。G在SLIME:C-C-C中,引用了一个未知的未定义函数,通常会得到一个警告

但是,加载文件e。G在SLIME中:具有多个顶级表单的C-C-k首先加载所有表单,然后才检查缺少的引用。在任何情况下,编译或加载时缺少引用都不是错误

这有点简化,但是CLHS章节非常通用,可以容纳非常不同的实现,在我看来,它提供的指导很少。但是,以上是一个基本的期望,不需要在单个文件中进行转发声明p> 除了Scheme&CL的特定语义之外,至少对于CL来说,Scheme&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)))))
那么,好的,这个函数在遍历文件时做什么

它会看到早期定义的form defun函数以及后来定义的函数,并对其求值。对defun表单求值定义了一个函数,该函数在早期定义,而该函数在被调用时将调用后来定义的尚未定义的函数。但是函数没有被调用,所以没有问题。 它看到后面定义的defun函数1并对其求值,后者定义但不调用后面定义的函数。 它可以看到早期定义的print函数,它调用早期定义的函数,然后调用后来定义的函数,这两个函数都已定义,并打印结果。 所以你可以看到,事实上,没有函数在定义之前被调用。函数在定义它们调用的函数之前定义,但这些函数在定义时不被调用

另外,如果您想在语言中允许递归,那么在定义函数时,这种前向引用的定义几乎是不可避免的。考虑这个阶乘函数的可怕定义:

(defun fact (n)
  (if (= n 1)
      1
    (* n (fact (1- n)))))
当系统评估这个定义来定义事实时,它将看到一个对事实的调用,这个调用。。。还没有定义。好的,也许您可以在特殊情况下允许this和CL编译器这样做:假设该调用实际上是对您定义的函数的调用

因此,可以使用只递归调用自身的特殊情况函数。但是,一旦有两个或多个函数递归地相互调用,就无法避免其中一个函数,在定义为未调用时!,指一些尚未定义的函数。因此,定义时的正向引用问题几乎是不可避免的


事实上,你可以避免它:你可以用Y组合符或其他什么东西来做你所有的递归,但尽管这在理论上很有趣,并且可以为家庭作业问题提供不可理解的答案,但在实践中没有人愿意这样做。

除了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)))))
那么,好的,这个函数在遍历文件时做什么

它会看到早期定义的form defun函数以及后来定义的函数,并对其求值。对defun表单求值定义了一个函数,该函数在早期定义,而该函数在被调用时将调用后来定义的尚未定义的函数。但是函数没有被调用,所以没有问题。 它看到后面定义的defun函数1并对其求值,后者定义但不调用后面定义的函数。 它可以看到早期定义的print函数,它调用早期定义的函数,然后调用后来定义的函数,这两个函数都已定义,并打印结果。 所以你可以看到,事实上,没有函数在定义之前被调用。函数在定义它们调用的函数之前定义,但这些函数在定义时不被调用

另外,如果您想在语言中允许递归,那么在定义函数时,这种前向引用的定义几乎是不可避免的。考虑这个阶乘函数的可怕定义:

(defun fact (n)
  (if (= n 1)
      1
    (* n (fact (1- n)))))
当系统评估这个定义来定义事实时,它将看到一个对事实的调用,这个调用。。。还没有定义。好的,也许您可以在特殊情况下允许this和CL编译器这样做:假设该调用实际上是对您定义的函数的调用

因此,可以使用只递归调用自身的特殊情况函数。但是,一旦有两个或多个函数递归地相互调用,就无法避免其中一个函数,在定义为未调用时!,指一些尚未定义的函数。因此,定义时的正向引用问题几乎是不可避免的


事实上,你可以避免它:你可以用Y组合符或其他什么东西来做你所有的递归,但尽管这在理论上很有趣,并且可以为家庭作业问题提供不可理解的答案,但在实践中没有人愿意这样做。

Common Lisp本质上是一个汇编程序。它是非常动态的。考虑下面的CLISP互动:

[1] >定义foo x定义bar y x 福 [2] >酒吧4 ***-EVAL:未定义的功能栏 以下重新启动可用: 使用值:R1您可以输入一个要使用的值,而不是FDEFINITION ”“酒吧。 重试:R2重试 商店价值 E:R3您可以为FDEFINITION'栏输入一个新值。 中止:R4中止 中断1[3]>:r4 [4] >富3 3. [5] >酒吧4 4. [6] >除氟x杆+1 x 福 [7] >富3 4. [8] >除雾杆x+2 x 酒吧 [9] >富3 6. [10]> 它只获取函数调用时生效的函数的最新定义并使用它。正如我们刚才看到的,您可以自由地重新定义您的函数,如果从其他函数中引用新版本,通常会调用新版本

阴谋/骗局是完全不同的事情。它的本质是静态的。任何函数引用都是通过使用环境来解析的。如果在嵌套环境中重新定义函数,以后如果允许它这样做,那么如果引用了原始版本,它仍将被调用

球拍源文件的顶级功能都属于同一个环境。如果某个函数被定义为调用另一个函数,而另一个函数实际上并不在同一作用域中定义,那么在源文件的某个地方(如果不是在某个库中)尝试加载源文件实际上是一个错误


Common Lisp很乐意加载这样的文件,因为用户以后总是可以通过任何方式定义缺少的函数,而且还有很多方法可以使用。

Common Lisp本质上是一个汇编程序。它是非常动态的。考虑下面的CLISP互动:

[1] >定义foo x定义bar y x 福 [2] >酒吧4 ***-EVAL:未定义的功能栏 以下重新启动可用: 使用值:R1您可以输入一个要使用的值,而不是FDEFINITION ”“酒吧。 重试:R2重试 存储值:R3您可以为FDEFINITION'栏输入新值。 中止:R4中止 中断1[3]>:r4 [4] >富3 3. [5] >酒吧4 4. [6] >除氟x杆+1 x 福 [7] >富3 4. [8] >除雾杆x+2 x 酒吧 [9] >富3 6. [10]> 它只获取函数调用时生效的函数的最新定义并使用它。正如我们刚才看到的,您可以自由地重新定义您的函数,如果从其他函数中引用新版本,通常会调用新版本

阴谋/骗局是完全不同的事情。它的本质是静态的。任何函数引用都是通过使用环境来解析的。如果在嵌套环境中重新定义函数,以后如果允许它这样做,那么如果引用了原始版本,它仍将被调用

球拍源文件的顶级功能都属于同一个环境。如果某个函数被定义为调用另一个函数,而另一个函数实际上并不在同一作用域中定义,那么在源文件的某个地方(如果不是在某个库中)尝试加载源文件实际上是一个错误


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

函数体在被调用之前不会被评估,如果在编译文件时调用未知函数,可能会收到编译器警告。这与Clojure不同,Clojure定义函数,调用未定义的函数是一个错误。@RainerJoswig你是认真的吗?但他们坚持认为这是一个Lisp。函数体在被调用之前不会被计算-如果在编译文件时调用未知函数,可能会收到编译器警告。这与Clojure不同,Clojure定义函数、调用未定义函数是错误的。@RainerJoswig你是认真的吗?然而,他们坚持认为,如果源文件中的每个函数使用未定义的函数,则在加载过程中会发出Lisp.SBCL警告。slime中的c-c-k编译文件,然后加载文件。文件编译器会将警告延迟到最后,但在加载期间,如果源文件中的每个函数使用未定义的函数,则loader.SBCL不会对其发出警告。slime中的c-c-k编译文件,然后加载文件。文件编译器将警告延迟到最后,但不会延迟加载程序。