另外,即使我可以使用 Common Lisp,我应该吗?方案更好吗?
5 回答
您在这里有几个答案,但没有一个是真正全面的(我不是在谈论有足够的细节或足够长)。首先,底线是:如果您想获得良好的 SICP 体验,您不应该使用 Common Lisp。
如果你对 Common Lisp 了解不多,那么就这样吧。(显然你可以忽略这个建议,有些人只能通过艰难的方式学习。)
如果您已经了解 Common Lisp,那么您可能会成功,但要付出相当大的努力,并且会对您的整体学习体验造成相当大的损害。有一些基本问题将 Common Lisp 和 Scheme 分开,这使得尝试将前者与 SICP 结合使用是一个非常糟糕的主意。事实上,如果您具备使其工作的知识水平,那么无论如何您都可能超过 SICP 的水平。我并不是说这不可能——当然可以在 Common Lisp 中实现整本书(例如,参见 Bendersky 的页面),就像在 C 或 Perl 或其他任何东西中一样。与 Scheme 相距甚远的语言只会变得更加困难。(例如,ML 可能比 Common Lisp 更容易使用,即使它的语法非常不同。)
以下是其中一些主要问题,按重要性递增的顺序排列。(我并不是说这个列表是详尽无遗的,我确信我在这里省略了一大堆额外的问题。)
NIL
和相关的问题,以及不同的名称。动态范围。
尾调用优化。
函数和值的单独命名空间。
我现在将扩展这些要点:
第一点是最技术性的。在 Common Lisp 中,NIL
既用作空列表又用作 false 值。就其本身而言,这并不是什么大问题,事实上 SICP 的第一版也有类似的假设——空列表和 false 是相同的值。然而,Common LispNIL
仍然不同:它也是一个符号。所以,在 Scheme 中你有一个明确的区分:某物要么是一个列表,要么是一种原始类型的值——但在 Common Lisp 中,NIL
它不仅是 false 和空列表:它也是一个符号。除此之外,你会得到许多稍微不同的行为——例如,在 Common Lisp 中,头部和尾部(car
和cdr
) 的空列表本身就是空列表,而在 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)
(或者更准确地说,你需要使用mapcar
Common Lisp 的car
函数等价物);您需要知道的另一件事是let
绑定名称的值槽,并labels
绑定函数槽(并且具有非常不同的语法,就像defun
和defvar
.) 但是所有这一切的概念结果是,Common Lispers 倾向于使用比 Schemers 少得多的高阶代码,并且从每种语言中常见的习语一直到实现将用它做什么。(例如,许多 Common Lisp 编译器永远不会优化这个 call: (funcall foo bar)
,而 Scheme 编译器会(foo bar)
像任何函数调用表达式一样优化,因为没有其他方法可以调用函数。)
最后,我要指出,以上大部分内容都是非常好的引战材料:将这些问题中的任何一个扔到公共 Lisp 或 Scheme 论坛(特别是comp.lang.lisp
and comp.lang.scheme
),你很可能会看到一个长线程,人们解释他们为什么选择比另一个好得多,或者为什么某些“所谓的功能”实际上是由当时显然非常醉酒的语言设计者做出的一个愚蠢的决定,等等。但问题是这些只是两者之间的差异两种语言,最终人们可以用任何一种语言完成工作。碰巧的是,如果工作是“做 SICP”,那么从 Scheme 的角度考虑它是如何解决这些问题的,Scheme 会容易得多。如果你想学习 Common Lisp,
将 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 机器),它进行了一些调试/跟踪/更加努力,...)。
有三种方法可以解决这个问题:
- 希望栈不会爆。坏的。
- 使用支持 TCO 的 Common Lisp 实现。有一些。见下文。
- 使用 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?
在numberp
CL 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-stream
和cons-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
你已经知道一些 Common Lisp 了吗?我想这就是你所说的“Lisp”的意思。在这种情况下,您可能想使用它而不是 Scheme。如果您也不知道,并且您只是为了学习经验而通过 SICP 工作,那么使用 Scheme 可能会更好。它为新学习者提供了更好的支持,您不必从 Scheme 翻译到 Common Lisp。
有区别;具体来说,SICP 的高功能风格在 Common Lisp 中更为冗长,因为您必须在传递函数时引用函数并使用它funcall
来调用绑定到变量的函数。
但是,如果您想使用 Common Lisp,您可以尝试使用Eli Bendersky 的 Common Lisp 对 SICP 代码的 SICP标签下的翻译。
它们相似但不相同。
我相信如果您使用 Scheme 会更容易。
编辑:内森桑德斯的评论是正确的。自从上次看这本书显然已经有一段时间了,但我只是检查了一下,它并没有call/cc
直接使用。我赞成内森的回答。
无论您使用什么都需要实现延续,而 SICP 经常使用该延续。甚至不是所有的 Scheme 解释器都实现了它们,而且我不知道有任何 Common Lisp 实现了它们。