似乎只有 lisp 允许使用变量而不先定义它?

It seems only lisp allow use a variable without define it first?

我发现好像只有lisp可以这样定义:

(lambda (x)(+ x y 1))

在其他语言中,即使是函数式语言,变量也必须先定义,然后才能使用

所以在lisp中有"free variable","bound variable"的概念。其他语言有这个概念吗?因为所有的变量都要先定义,所以所有的变量都是"bound variable"?变量有一个初始值,在全局或其他范围内?

我觉得lambda的概念很早,但是如果任何变量,或者值,必须先定义再使用,不是更容易理解和使用吗?

谢谢!

在 Common Lisp 中,使用未定义变量的确切效果是未定义的。

Common Lisp 编译器会发出警告:

* (lambda (x) (+ x y 1))

; in: LAMBDA (X)
;     (+ X Y 1)
; --> + 
; ==>
;   (+ X Y)
; 
; caught WARNING:
;   undefined variable: COMMON-LISP-USER::Y
; 
; compilation unit finished
;   Undefined variable:
;     Y
;   caught 1 WARNING condition
#<FUNCTION (LAMBDA (X)) {226A95BB}>

我认为这个问题有两个部分。在 Lisp(和其他语言)中有一个关于自由变量访问的普遍问题,这是一个很大的主题,因为有很多很多 Lisp 甚至更多其他语言,包括对某些变量(CL)具有动态范围的语言和具有 动态范围(elisp 直到最近,许多其他旧实现)。还有一个问题是关于何时需要在 Lisps(和其他语言)中解析变量引用,特别是需要前向引用以及何时以及如何解析它们..

在这个回答中,我只讨论第二部分,关于前向引用,我认为这是直接让您感到困惑的地方。我将使用 Racket 和 r5rs 语言给出示例,因为这就是您在评论中使用的语言。

首先,任何支持递归而不跳过极端循环的语言(aka 使用 Y 组合器)需要支持对尚未绑定的名称的引用,这些名称是称为转发引用。考虑一下:

#lang r5rs

(define foo
  (lambda (x)
    (if (null? x)
        0
        (+ 1 (foo (cdr x))))))

OK,所以当函数被评估时,有一个对 foo 的引用用于递归调用。 但是 foo 在那一点上还没有绑定 ,因为 foo 将绑定到函数本身,这就是我们定义的。事实上,如果你这样做:

(let ((r (lambda (x)
           (if (null? x)
               0
               (+ 1 (r (cdr x)))))))
  r)

这是一个错误,因为r确实还没有定义。相反,您需要使用 letrec 来执行适当的魔法以确保一切正常。

嗯,你可能会争辩说 letrec 变成了这样的东西:

(let ((r #f))
  (set! r (lambda (x)
            (if (null? x)
                0
                (+ 1 (r (cdr x))))))
  r)

也许确实如此。但是这个呢?

#lang r5rs

(define foo
  (lambda (x)
    (if (null? x)
        0
        (bar x))))

(define bar
  (lambda (x)
    (+ 1 (foo (cdr x)))))

还有更详细的程序。

所以这里有一个关于前向引用的普遍问题。基本上不可能以这样一种方式编写程序,即在程序的文本中,没有对未知名称的引用。注意上面程序中 foo 指的是 bar bar 指的是 foo 所以有 no foobar 定义的 order 不涉及对尚未在源中绑定的名称的引用。

请注意,此问题基本上影响所有编程语言:这不是 Lisp-family 语言独有的。

那么,这个问题是如何解决的呢?嗯,答案是'it depends on how the specific language you care about is defined'。我不完全确定 R5RS 规范对此有何评论,但我想我 确定 Racket 对此有何评论。

Racket说的是前向引用都需要在模块级别进行整理。所以如果我有这样的球拍源文件:

#lang r5rs

(define a
  (lambda (x) (+ x y)))

(define y 1)

这很好,因为在模块的末尾同时定义了ax,所以[=36=的定义】 还好。这与上面foobar的定义没有区别,注意源文件是这样的:

#lang r5rs

(define foo
  (lambda (x)
    (if (null? x)
        0
        (bar x))))

不合法:bar 未绑定:

$ racket ts.rkt
ts.rkt:7:9: bar: unbound identifier
[...]

所以forward-reference问题的答案是two-fold:

  • 用几乎所有实用语言编写程序,尤其是在 Lisp 中,需要对尚未在引用点绑定的名称进行前向引用;
  • 语言规范需要定义何时以及如何解析此类前向引用。

我认为这就是让您感到困惑的地方。特别是一个 Racket 模块,它只包含这个:

#lang r5rs

(define a (lambda (x) (+ x y)))

在 Racket 中是不合法的。但是这个模块:

#lang r5rs

(define a (lambda (x) (+ x y)))
(define y 1)

是合法的,因为 y 是在解析前向引用时绑定的。


Common Lisp 比这更复杂,原因有二:

  • 它是 Lisp-2,所以函数和变量绑定存在于不同的命名空间中;
  • 没有标准的顶层词法变量绑定。

例如,如果我们采取这样的事情(假设在顶层,前面没有其他用户定义):

(defun foo (x)
  (+ x y))

那么y 不能是词法变量的引用,因为foo的词法环境是空的,因为没有顶层词汇变量绑定。

所以y 必须是对动态变量(CL 语言中的特殊变量)的引用。但实际上,CL通过声明不允许这样的引用来解决动态变量的forward-reference问题。所以上面的定义是不合法的CL。许多编译器会在编译时发出警告,但他们不必这样做。

但是,以下变体很好(请注意,我对动态变量使用了 CL 约定)。

(defvar *y* 1)

(defun foo (x)
  (+ x *y*))

这很好,因为 *y*foo 定义之前就已经知道了。

事实上我撒了谎:转发对动态变量的引用允许的,但你必须告诉语言米:

(defun foo (x)
  (declare (special *y*))
  (+ x *y*))

而现在,如果稍后有 (defvar *y* ...)(或等效)调用 foo 就可以了。如果 *y* 没有这样的 globally-special 声明,那么我认为如果调用 foo 会发生什么未定义或错误(我有点希望是后者,但我不确定.

最后,即使没有*y*的全球特别声明,这也很好:

(let ((*y* 3))
  (declare (special *y*))
  (foo 1))

这很好,因为 *y* 在本地声明为特殊的(有一个绑定声明)。

在 CL 中允许对 函数 的转发引用,关于何时需要解析它们有复杂的规则,我不确定我是否记得。

关于首先定义的规则有一些例外,对于所有这些规则,他们在提及某个标准值时定义它们。例如。 JavaScript 使用 undefined,perl 使用 undef,等等

绑定变量是当前函数范围内的变量。自由变量来自嵌套函数作用域或全局变量,但它们需要在运行时存在。

(define test 
  ((lambda (a) 
     (lambda (b) 
       (list g a b)))
    5))
(define g 0)
(test 10)

所以在内部函数中有 b 作为绑定变量。 ga 是自由变量,因为它们不是来自该函数参数。这就是 free 的意思,并且 g 不需要在函数中的 mention 之前定义,但需要在调用 mentions.

的代码之前存在。

在词法范围的语言中,5 将绑定到 a,这样在动态范围的语言中,结果将是 (0 5 10),例如。 PicoLisp 的语法略有不同,答案是 (0 2 10),因为创建 test 时的绑定 a 在其内部代码完成后的第二个范围内就超出了范围。

几乎所有的语言都是词法范围的。例如。所有 OP 语言标签(Ocaml、F#、Haskell、C# 和 Clojure)都是词法范围的。