46

另外,即使我可以使用 Common Lisp,我应该吗?方案更好吗?

4

5 回答 5

118

您在这里有几个答案,但没有一个是真正全面的(我不是在谈论有足够的细节或足够长)。首先,底线是:如果您想获得良好的 SICP 体验,您不应该使用 Common Lisp。

如果你对 Common Lisp 了解不多,那么就这样吧。(显然你可以忽略这个建议,有些人只能通过艰难的方式学习。)

如果您已经了解 Common Lisp,那么您可能会成功,但要付出相当大的努力,并且会对您的整体学习体验造成相当大的损害。有一些基本问题将 Common Lisp 和 Scheme 分开,这使得尝试将前者与 SICP 结合使用是一个非常糟糕的主意。事实上,如果您具备使其工作的知识水平,那么无论如何您都可能超过 SICP 的水平。我并不是说这不可能——当然可以在 Common Lisp 中实现整本书(例如,参见 Bendersky 的页面),就像在 C 或 Perl 或其他任何东西中一样。与 Scheme 相距甚远的语言只会变得更加困难。(例如,ML 可能比 Common Lisp 更容易使用,即使它的语法非常不同。)

以下是其中一些主要问题,按重要性递增的顺序排列。(我并不是说这个列表是详尽无遗的,我确信我在这里省略了一大堆额外的问题。)

  1. NIL和相关的问题,以及不同的名称。

  2. 动态范围。

  3. 尾调用优化。

  4. 函数和值的单独命名空间。

我现在将扩展这些要点:

第一点是最技术性的。在 Common Lisp 中,NIL既用作空列表又用作 false 值。就其本身而言,这并不是什么大问题,事实上 SICP 的第一版也有类似的假设——空列表和 false 是相同的值。然而,Common LispNIL仍然不同:它也是一个符号。所以,在 Scheme 中你有一个明确的区分:某物要么是一个列表,要么是一种原始类型的值——但在 Common Lisp 中,NIL它不仅是 false 和空列表:它也是一个符号。除此之外,你会得到许多稍微不同的行为——例如,在 Common Lisp 中,头部和尾部(carcdr) 的空列表本身就是空列表,而在 Scheme 中,如果您尝试这样做,则会出现运行时错误。最重要的是,您有不同的名称和命名约定,例如——Common Lisp 中的谓词按约定以P(例如,listp)结尾,而Scheme 中的谓词以问号结尾(例如,list?);Common Lisp 中的mutators 没有特定的约定(有些有N前缀),而在Scheme 中它们几乎总是有一个后缀!. 此外,Common Lisp 中的普通赋值通常是 setf并且它也可以对组合进行操作(例如,(setf (car foo) 1)),而在 Scheme 中它set!仅限于设置绑定变量。(请注意,Common Lisp 也有限制版本,它被称为setq. 但几乎没有人使用它。)

第二点是更深层次的一点,并且可能会导致您的代码完全无法理解的行为。问题是在 Common Lisp 中,函数参数是词法范围的,但声明的变量defvar动态范围的。有很多依赖于词法范围绑定的解决方案——而在 Common Lisp 中它们根本行不通。当然,Common Lisp具有词法范围这一事实意味着您可以通过对新绑定非常小心,并可能使用宏来绕过默认动态范围来解决这个问题——但同样,这需要比这更广泛的知识。一个典型的新手都有。事情变得更糟:如果你声明一个特定的名字defvar, 然后该名称将被动态绑定,即使它们是函数的参数。这可能导致一些极其难以跟踪的错误,这些错误以极其令人困惑的方式表现出来(您基本上得到了错误的值,而且您不知道为什么会发生这种情况)。有经验的 Common Lispers 知道它(尤其是那些被它烧毁的),并且总是遵循在动态范围名称周围使用星号的约定(例如,*foo*)。(顺便说一下,在 Common Lisp 行话中,这些动态范围的变量被称为“特殊变量”——这对新手来说是另一个混​​淆的来源。)

之前的一些评论中也讨论了第三点。事实上,Rainer 对你所拥有的不同选项做了一个很好的总结,但他没有解释它到底有多难。问题是适当的尾调用优化 (TCO) 是 Scheme 中的基本概念之一。重要的是它是一种语言特性,而不仅仅是一种优化。Scheme 中的典型循环表示为尾部调用函数(例如(define (loop) (loop))),并且需要适当的 Scheme 实现实现 TCO,这将保证这实际上是一个无限循环,而不是运行一小段时间,直到你炸毁堆栈空间。这就是 Rainer 第一个非解决方案的全部精髓,也是他将其标记为“BAD”的原因。他的第三个选择——将函数循环(表示为递归函数)重写为 Common Lisp 循环(dotimes,dolist和臭名昭著loop的TCO 不仅是语言的基础——它也是本书的主要主题之一,所以这样做,你将完全失去这一点。此外,有些情况是您无法做到的将 Scheme 代码翻译成 Common Lisp 循环结构——例如,当您阅读本书时,您将实现一个元循环解释器,它是一种迷你方案语言的实现。需要点击一下才能意识到这个元评估器实现了一种语言,它本身就在做 TCO ,如果您实现此评估器的语言本身就是 TCO。(请注意,我说的是“简单”解释器——在本书后面的部分中,您将此评估器实现为类似于寄存器机器的东西,您可以明确地让它做 TCO。)所有这一切的底线,是这个评估器 - 当在 Common Lisp 中实现时 - 将导致语言本身不执行 TCO。熟悉这一切的人应该不会感到惊讶:毕竟,评估器的“循环性”意味着您正在实现一种语义非常接近宿主语言的语言——所以在这种情况下,您“继承" Common Lisp 语义而不是 Scheme TCO 语义。但是,这意味着您的小型评估器现在已经瘫痪:它没有 TCO,所以它没有办法做循环!要获得循环,您需要在解释器中实现新的结构,这通常会使用 Common Lisp 中的迭代结构。但现在你离书中的内容更远了,你投入了相当大的努力将 SICP 中的想法大致实现到不同的语言。另请注意,所有这些都与我提出的前一点有关:如果您遵循本书,那么您实现的语言将在词法范围内,使其远离 Common Lisp 宿主语言。所以总的来说,你完全失去了书中所说的“元循环评估器”中的“循环”属性。(同样,这可能不会打扰您,但会损害整体学习体验。)总而言之,很少有语言能够接近 Scheme 能够在语言内部实现语言的语义作为非琐碎(例如,不使用eval)评估容易地。事实上,如果你确实使用 Common Lisp,那么在我看来,Rainer 的第二个建议——使用支持 TCO 的 Common Lisp 实现——是最好的选择。然而,在 Common Lisp 中,这基本上是一种编译器优化:因此,您可能需要 (a) 了解实现中需要转向以实现 TCO 的旋钮,(b) 您需要确保 Common Lisp 实现实际上是在做适当的 TCO,而不仅仅是优化自我调用(这是一个更简单的情况,几乎没有那么重要),(c)你希望执行 TCO 的 Common Lisp 实现可以在不破坏调试选项的情况下这样做(同样,由于这被认为是 Common Lisp 中的优化,然后打开此旋钮,编译器也可能会认为“我不太在意可调试性”)。

最后,我的最后一点并不难克服,但它在概念上是最重要的。在 Scheme 中,你有一个统一的规则:标识符有一个值,它是由词法确定的——就是这样。这是一种非常简单的语言。在 Common Lisp 中,除了有时使用动态作用域和有时使用词法作用域的历史包袱之外,您还有具有两个不同值的符号——每当变量出现在表达式的头部时,就会使用函数值,并且有一个不同的值用于其他情况。例如,在 中(foo foo), 的两个实例中的每一个foo都有不同的解释——第一个是 的函数值foo第二个是它的变量值。同样,这并不难克服——您需要了解许多结构来处理所有这些问题。例如,(lambda (x) (x x))你需要写而不是写(lambda (x) (funcall x x)),这使得被调用的函数出现在变量位置,因此在那里将使用相同的值;另一个例子是(map car something)你需要翻译的(map #'car something)(或者更准确地说,你需要使用mapcarCommon Lisp 的car函数等价物);您需要知道的另一件事是let绑定名称的值槽,并labels绑定函数槽(并且具有非常不同的语法,就像defundefvar.) 但是所有这一切的概念结果是,Common Lispers 倾向于使用比 Schemers 少得多的高阶代码,并且从每种语言中常见的习语一直到实现将用它做什么。(例如,许多 Common Lisp 编译器永远不会优化这个 call: (funcall foo bar),而 Scheme 编译器会(foo bar)像任何函数调用表达式一样优化,因为没有其他方法可以调用函数。)

最后,我要指出,以上大部分内容都是非常好的引战材料:将这些问题中的任何一个扔到公共 Lisp 或 Scheme 论坛(特别是comp.lang.lispand comp.lang.scheme),你很可能会看到一个长线程,人们解释他们为什么选择比另一个好得多,或者为什么某些“所谓的功能”实际上是由当时显然非常醉酒的语言设计者做出的一个愚蠢的决定,等等。但问题是这些只是两者之间的差异两种语言,最终人们可以用任何一种语言完成工作。碰巧的是,如果工作是“做 SICP”,那么从 Scheme 的角度考虑它是如何解决这些问题的,Scheme 会容易得多。如果你想学习 Common Lisp,

于 2009-07-23T01:47:49.220 回答
18

将 SICP 与 Common Lisp 结合使用是可行且有趣的

您可以使用 Common Lisp 来学习 SICP 而不会遇到太多问题。本书中使用的 Scheme 子集不是很复杂。SICP 不使用宏,也不使用延续。有 DELAY 和 FORCE,用 Common Lisp 几行就可以写出来。

同样对于使用(function foo) and (funcall foo 1 2 3) 实际上更好的初学者(恕我直言!),因为在学习函数式编程部分时代码会变得更清晰。你可以看到变量和 lambda 函数在哪里被调用/传递。

Common Lisp 中的尾调用优化

使用 Common Lisp 只有一个很大的方面存在缺陷:尾调用优化(TCO)。Common Lisp 在其标准中不支持 TCO(因为与其他语言的交互不清楚,并非所有计算机体系结构都直接支持它(想想 JVM),并非所有编译器都支持它(一些 Lisp 机器),它进行了一些调试/跟踪/更加努力,...)。

有三种方法可以解决这个问题:

  1. 希望栈不会爆。坏的。
  2. 使用支持 TCO 的 Common Lisp 实现。有一些。见下文。
  3. 使用 DOTIMES、DO、LOOP、...将功能循环(和类似结构)重写为循环(和类似结构)

个人推荐2或3。

Common Lisp 具有出色且易于使用的编译器,支持 TCO(SBCL、LispWorks、Allegro CL、Clozure CL,...),并且作为开发环境使用内置编译器或 GNU Emacs/SLIME。

对于与 SICP 一起使用,我会推荐SBCL,因为它总是默认编译,默认支持 TCO 并且编译器会捕获很多编码问题(未声明的变量、错误的参数列表、一堆类型错误……)。这对学习有很大帮助。通常确保代码已编译,因为 Common Lisp 解释器通常不支持 TCO。

有时编写一两个宏并提供一些 Scheme 函数名称以使代码看起来更像 Scheme 也可能会有所帮助。例如,您可以在 Common Lisp 中使用 DEFINE 宏。

对于更高级的用户,有一个用 Common Lisp 编写的旧 Scheme 实现(称为 Pseudo Scheme),它应该运行 SICP 中的大部分代码。

我的建议:如果你想加倍努力并使用 Common Lisp,那就去做吧。

为了更容易理解必要的更改,我添加了一些示例 - 请记住,它需要一个支持尾调用优化的 Common Lisp 编译器:

例子

让我们看一下 SICP 中的这个简单代码:

(define (factorial n)
  (fact-iter 1 1 n))

(define (fact-iter product counter max-count)
  (if (> counter max-count)
      product
      (fact-iter (* counter product)
                 (+ counter 1)
                 max-count)))

DEFINE我们可以通过宏直接在 Common Lisp 中使用它:

(defmacro define ((name &rest args) &body body)
  `(defun ,name ,args ,@body))

现在你应该使用 SBCL、CCL、Allegro CL 或 LispWorks。这些编译器默认支持 TCO。

让我们使用 SBCL:

* (define (factorial n)
    (fact-iter 1 1 n))
; in: DEFINE (FACTORIAL N)
;     (FACT-ITER 1 1 N)
; 
; caught STYLE-WARNING:
;   undefined function: FACT-ITER
; 
; compilation unit finished
;   Undefined function:
;     FACT-ITER
;   caught 1 STYLE-WARNING condition

FACTORIAL
* (define (fact-iter product counter max-count)
    (if (> counter max-count)
        product
        (fact-iter (* counter product)
                   (+ counter 1)
                   max-count)))

FACT-ITER
* (factorial 1000)

40238726007709....

另一个例子:符号微分

SICP 有一个用于区分的 Scheme 示例:

(define (deriv exp var)
  (cond ((number? exp) 0)
        ((variable? exp)
         (if (same-variable? exp var) 1 0))
        ((sum? exp)
         (make-sum (deriv (addend exp) var)
                   (deriv (augend exp) var)))
        ((product? exp)
         (make-sum
           (make-product (multiplier exp)
                         (deriv (multiplicand exp) var))
           (make-product (deriv (multiplier exp) var)
                         (multiplicand exp))))
        (else
         (error "unknown expression type -- DERIV" exp))))

让这段代码在 Common Lisp 中运行很容易:

  • 有些函数有不同的名字,number?numberpCL
  • CL:COND使用T而不是else
  • CL:ERROR使用 CL 格式字符串

让我们为一些函数定义方案名称。常见的 Lisp 代码:

(loop for (scheme-symbol fn) in
      '((number?      numberp)
        (symbol?      symbolp)
        (pair?        consp)
        (eq?          eq)
        (display-line print))
      do (setf (symbol-function scheme-symbol)
               (symbol-function fn)))

从上面我们的define宏:

(defmacro define ((name &rest args) &body body)
  `(defun ,name ,args ,@body))

Common Lisp 代码:

(define (variable? x) (symbol? x))

(define (same-variable? v1 v2)
  (and (variable? v1) (variable? v2) (eq? v1 v2)))

(define (make-sum a1 a2) (list '+ a1 a2))

(define (make-product m1 m2) (list '* m1 m2))

(define (sum? x)
  (and (pair? x) (eq? (car x) '+)))

(define (addend s) (cadr s))

(define (augend s) (caddr s))

(define (product? x)
  (and (pair? x) (eq? (car x) '*)))

(define (multiplier p) (cadr p))

(define (multiplicand p) (caddr p))

(define (deriv exp var)
  (cond ((number? exp) 0)
        ((variable? exp)
         (if (same-variable? exp var) 1 0))
        ((sum? exp)
         (make-sum (deriv (addend exp) var)
                   (deriv (augend exp) var)))
        ((product? exp)
         (make-sum
           (make-product (multiplier exp)
                         (deriv (multiplicand exp) var))
           (make-product (deriv (multiplier exp) var)
                         (multiplicand exp))))
        (t
         (error "unknown expression type -- DERIV: ~a" exp))))

让我们在 LispWorks 中尝试一下:

CL-USER 19 > (deriv '(* (* x y) (+ x 3)) 'x)
(+ (* (* X Y) (+ 1 0)) (* (+ (* X 0) (* 1 Y)) (+ X 3)))

来自 Common Lisp 中 SICP 的流示例

参见SICP 3.5 章节的书籍代码。我们使用上面对 CL 的添加。

SICP 提到delay,the-empty-streamcons-stream, 但没有实现它。我们在 Common Lisp 中提供了一个实现:

(defmacro delay (expression)
  `(lambda () ,expression))

(defmacro cons-stream (a b)
  `(cons ,a (delay ,b)))

(define (force delayed-object)
  (funcall delayed-object))

(defparameter the-empty-stream (make-symbol "THE-EMPTY-STREAM"))

现在来自书中的可移植代码:

(define (stream-null? stream)
  (eq? stream the-empty-stream))

(define (stream-car stream) (car stream))

(define (stream-cdr stream) (force (cdr stream)))

(define (stream-enumerate-interval low high)
  (if (> low high)
      the-empty-stream
    (cons-stream
     low
     (stream-enumerate-interval (+ low 1) high))))

现在 Common Lisp 的不同之处在于stream-for-each

  • 我们需要使用cl:progn而不是begin
  • 函数参数需要调用cl:funcall

这是一个版本:

(defmacro begin (&body body) `(progn ,@body))

(define (stream-for-each proc s)
  (if (stream-null? s)
      'done
      (begin (funcall proc (stream-car s))
             (stream-for-each proc (stream-cdr s)))))

我们还需要使用以下方法传递函数cl:function

(define (display-stream s)
  (stream-for-each (function display-line) s))

但是这个例子有效:

CL-USER 20 > (stream-enumerate-interval 10 20)
(10 . #<Closure 1 subfunction of STREAM-ENUMERATE-INTERVAL 40600010FC>)

CL-USER 21 > (display-stream (stream-enumerate-interval 10 1000))

10 
11 
12 
...
997 
998 
999 
1000 
DONE
于 2009-07-21T14:37:49.610 回答
10

你已经知道一些 Common Lisp 了吗?我想这就是你所说的“Lisp”的意思。在这种情况下,您可能想使用它而不是 Scheme。如果您也不知道,并且您只是为了学习经验而通过 SICP 工作,那么使用 Scheme 可能会更好。它为新学习者提供了更好的支持,您不必从 Scheme 翻译到 Common Lisp。

有区别;具体来说,SICP 的高功能风格在 Common Lisp 中更为冗长,因为您必须在传递函数时引用函数并使用它funcall来调用绑定到变量的函数。

但是,如果您想使用 Common Lisp,您可以尝试使用Eli Bendersky 的 Common Lisp 对 SICP 代码的 SICP标签下的翻译。

于 2009-07-21T13:49:00.977 回答
2

它们相似但不相同。

我相信如果您使用 Scheme 会更容易。

于 2009-07-21T13:37:43.890 回答
1

编辑:内森桑德斯的评论是正确的。自从上次看这本书显然已经有一段时间了,但我只是检查了一下,它并没有call/cc直接使用。我赞成内森的回答。


无论您使用什么都需要实现延续,而 SICP 经常使用该延续。甚至不是所有的 Scheme 解释器都实现了它们,而且我不知道有任何 Common Lisp 实现了它们。

于 2009-07-21T13:41:12.373 回答