另外,即使我可以使用Common Lisp,我应该吗?方案更好吗?
你在这里有几个答案,但没有一个真正全面(我不是在谈论有足够的细节或足够长).首先,底线:你应该不会,如果你想与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的第一版有一个类似的假设 - 空列表和假值是相同的值.然而,Common Lisp NIL
仍然是不同的:它也是一个符号.因此,在Scheme中你有一个明确的分离:某个东西是列表,或者是一个原始类型的值 - 但在Common Lisp中,NIL
不仅是假和空列表:它也是一个符号.除此之外,你会得到一些稍微不同的行为 - 例如,在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 具有词法范围这一事实意味着你可以通过非常小心新的绑定来解决这个问题,并且可能使用宏来绕过默认的动态范围 - 但同样,这需要比一个典型的新手.事情变得更糟:如果你用a声明一个特定的名字defvar
,那么这个名字将被动态绑定,即使它们是函数的参数.这可能会导致一些非常难以跟踪的错误,这些错误以极其混乱的方式表现出来(你基本上得到了错误的价值,而且你也不知道为什么会发生这种情况).有经验的Common Lispers知道它(特别是那些被它烧过的人),并且总是遵循围绕动态范围名称使用星星的惯例(例如*foo*
).(顺便说一句,在Common Lisp行话中,这些动态范围的变量只被称为"特殊变量" - 这是新手混淆的另一个原因.)
在前面的一些评论中也讨论了第三点.事实上,Rainer对你所拥有的不同选项有一个非常好的总结,但他没有解释它有多么难以做到.问题在于,正确的尾调用优化(TCO)是Scheme中的基本概念之一.重要的是它是一种语言特征而不仅仅是一种优化.方案A中典型的循环被表示为尾调用函数(例如(define (loop) (loop))
)和适当的方案实现都需要实现TCO,这将保证这是,事实上,一个无限循环,而不是一小会儿运行,直到你吹堆栈空间.这是Rainer第一个非解决方案的本质,也是他将其标记为"BAD"的原因.他的第三个选择 - 重写函数循环(表示为递归函数)作为Common Lisp循环(dotimes
,dolist
和臭名昭着的loop
)可以适用于一些简单的情况,但成本非常高:Scheme是一种正确的语言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)你需要确保共同的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编译器永远不会优化此调用:(funcall foo bar)
,而Scheme编译器将(foo bar)
像任何函数调用表达式一样进行优化,因为没有其他方法可以调用函数.)
最后,我会注意到那么多的上面是非常好的flamewar材料:扔任何这些问题纳入公共Lisp或计划论坛(特别是comp.lang.lisp
和comp.lang.scheme
),你很可能会看到一个长螺纹人们解释为什么他们的选择远远好于另一个,或者为什么一些"所谓的特征"实际上是由当时显然非常醉酒等语言设计者做出的愚蠢决定等等.但问题是这些只是两种语言,最终人们可以在任何一种语言中完成工作.只是如果工作是"做SICP",那么考虑到如何从Scheme的角度解决这些问题,Scheme会更容易.如果你想学习Common Lisp,那么使用Common Lisp教科书会让你更加沮丧.
使用SICP和Common Lisp是可能和有趣的
您可以使用Common Lisp与SICP一起学习,而不会出现太多问题.本书中使用的Scheme子集不是很复杂.SICP不使用宏,它不使用延续.有DELAY和FORCE,可以用Common Lisp中的几行编写.
也适合初学者使用(function foo)
并且 (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(称为Pseudo Scheme)编写的旧的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)))
我们可以在Common Lisp中使用DEFINE
宏直接使用它:
(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格式字符串
让我们为一些函数定义Scheme名称.Common 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的Streams示例
请参阅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 . #) 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,您可以尝试在标签SICP下使用Eli Bendersky的SICP代码的Common Lisp翻译.