1

我发现似乎只有在 lisp 中才能定义这个:

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

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

所以在lisp中,有“自由变量”,“绑定变量”的概念。其他语言有这个概念吗?因为所有变量都必须先定义,所以所有变量都是“绑定变量”?变量有一个初始值,在全局或其他范围?

我认为 lambda 概念很早,但是如果有任何变量或值,必须先定义然后使用,是不是更容易理解和使用?

谢谢!

4

3 回答 3

5

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

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

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

#lang r5rs

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

好的,所以当函数被评估时,有一个foo递归调用的引用。 But foois not yet bound at that point,因为foo将被绑定到函数本身,这就是我们要定义的。事实上,如果你这样做:

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

这是一个错误,因为r确实尚未定义。相反,您需要使用letrecwhich 会产生合适的魔法来确保一切正常。

好吧,您可能会争辩说这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)))))

并 for on 以获得更详细的程序。

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

请注意,这个问题基本上会影响所有编程语言:这不是 Lisp 家族语言所独有的。

那么,这是如何解决的呢?嗯,答案是“这取决于你关心的特定语言是如何定义的”。我不完全确定 R5RS 规范对此有何评论,但我想我确信Racket 对此有何评论。

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

#lang r5rs

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

(define y 1)

这很好,因为在模块的末尾都定义了ax,所以定义a也很好。foo这与上面的定义没有什么不同bar,请注意像这样的源文件:

#lang r5rs

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

不合法:bar不受约束:

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

因此,前向引用问题的答案有两个:

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

我认为这让你感到困惑。特别是仅包含以下内容的 Racket 模块:

#lang r5rs

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

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

#lang r5rs

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

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


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

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

因此,例如,如果我们采用这样的方法(假设在顶层,之前没有其他用户定义):

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

theny 不能是对词法变量的引用,因为 的词法环境foo是空的,因为没有顶级词法变量绑定。

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

然而,下面的变体很好(注意我使用了动态变量的 CL 约定)。

(defvar *y* 1)

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

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

事实上我撒了谎:允许对动态变量前向引用,但你必须告诉语言它们:

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

现在,如果有以后(defvar *y* ...)(或同等)的电话foo会很好。如果没有这样的全球特殊声明,*y*那么我认为如果foo被调用是未定义或错误会发生什么(我有点希望后者,但我不确定。

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

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

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

CL 中允许对函数的前向引用,并且关于何时需要解决它们有一些复杂的规则,我不确定我是否记得。

于 2019-07-03T10:48:50.897 回答
3

在 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}>
于 2019-07-02T08:33:18.647 回答
2

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

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

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

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

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

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

于 2019-07-04T22:38:00.503 回答