我可以将Common Lisp用于SICP吗?还是Scheme是唯一的select?
另外,即使我可以使用Common Lisp,应该吗? 计划更好吗?
你在这里有几个答案,但没有一个是真正的全面的(我不是说有足够的细节或足够长)。 首先,底线是:如果你想对SICP有一个很好的经验,你不应该使用Common Lisp。
如果你不太了解Common Lisp,那就把它当成是。 (显然你可以不考虑这个build议,有些人只会学习困难。)
如果您已经了解了Common Lisp,那么您可能需要付出相当大的努力,并对整体学习体验造成相当大的损害。 Common Lisp和Scheme有一些基本的问题,它们试图在SICP中使用前者是一个非常糟糕的主意。 事实上, 如果你的知识水平能够达到这个水平,那么你就有可能超过SICP的水平。 我并不是说这是不可能的 – 当然也可以在Common Lisp中实现整本书(例如,参见Bendersky的页面),就像你可以在C或者Perl中这样做。 语言与计划之间的差距越来越大。 (例如,ML可能比Common Lisp更容易使用,即使它的语法有很大不同。)
以下是这些重要问题中的一些重要问题。 (我并不是说这个清单是完整无缺的,我敢肯定还有一大堆我在这里省略的其他问题。)
-
NIL
和相关的问题,以及不同的名称。 -
dynamic范围。
-
尾巴呼叫优化。
-
为函数和值分别命名空间。
我现在将扩展到以下每一点:
第一点是最技术的。 在Common Lisp中, NIL
既用作空列表又用作假值。 其本身并不是一个大问题,实际上SICP的第一版也有类似的假设 – 空列表和假列表是相同的值。 然而,Common Lisp的NIL
仍然不同:它也是一个象征。 因此,在Scheme中你有一个明确的分离:东西是一个列表,或者是一个基本types的值 – 但是在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!
仅限于设置绑定variables。 (请注意,Common Lisp也有限制版本,它被称为setq
,几乎没有人使用它。)
第二点是更深的一个,可能会导致你的代码完全不可理解的行为。 问题是,在Common Lisp中,函数参数在词汇范围内,但是用defvar
声明的variables是dynamic范围的。 有一整套解决scheme依赖于词汇范围的绑定 – 而在Common Lisp中,它们就不起作用。 当然,Common Lisp 具有词汇范围的事实意味着你可以通过对新的绑定非常小心,并可能使用macros来绕过默认的dynamic范围来解决这个问题 – 但是,这又需要更广泛的知识一个典型的新手有。 事情变得更糟:如果你用defvar
声明了一个特定的名字,那么这个名字将被dynamic地绑定, 即使它们是函数的参数。 这可能会导致一些非常难以追踪的错误,这些错误以非常混乱的方式performance出来(你基本上得到了错误的价值,而你却不知道为什么会这样)。 经验丰富的Common Lispers知道它(尤其是那些被它烧死的),并且总是遵循在dynamic范围名称(例如*foo*
)周围使用星号的惯例。 (顺便说一下,在Common Lisp行话中,这些dynamic范围的variables被称为“特殊variables” – 这是新手混淆的另一个来源。)
前面的一些评论也讨论了第三点。 事实上,Rainer对于你有不同的select有一个相当不错的总结,但是他没有解释它能做出多less努力。 问题在于适当的tail-call-optimization(TCO)是Scheme中的一个基本概念。 重要的是它是一种语言function,而不仅仅是一种优化。 Scheme中典型的循环表示为尾部调用函数(例如, (define (loop) (loop))
),需要合适的Scheme实现来实现TCO,这将确保这实际上是一个无限循环比运行一会儿,直到你炸了堆栈空间。 这就是Rainer第一个非解决scheme的精髓所在,也是他将其标记为“BAD”的原因。 他的第三个select – 重写函数循环(表示为recursion函数)作为Common Lisp循环( dotimes
, dolist
和臭名昭着的loop
)可以用于几个简单的情况,但代价非常高:Scheme是一种语言那么合适的TCO不仅是语言的基础 – 这也是本书的主要内容之一,所以这样做,你就完全失去了这一点。 此外,在某些情况下,您不能将Scheme代码翻译为Common Lisp循环结构 – 例如,当您在本书中进行工作时,您将会实现一个元循环解释器,这是一个实现的迷你计划语言。 需要一定的点击才能意识到, 如果您实施此评估程序的语言本身正在执行TCO,则此元评估程序将实施自身执行TCO的语言。 (请注意,我正在谈论“简单”的解释器 – 在本书的后面,你将这个评估器实现为一个接近注册机器的东西,在那里你明确地使它做TCO。)所有这一切的底线,是这个评估者 – 当在Common Lisp中实现时 – 将导致一种本身不会执行TCO的语言。 熟悉所有这些的人不应该感到惊讶:毕竟,评估者的“循环性”意味着你正在实现一种非常接近主语言的语义 – 所以在这种情况下,你“inheritance“Common Lisp语义而不是Scheme TCO语义。 但是,这意味着您的迷你评估者现在已经陷入瘫痪:它没有TCO,所以它没有办法做循环! 为了得到循环,你需要在你的解释器中实现新的构造,它通常使用Common Lisp中的迭代构造。 但是现在你要远离书中的内容,而且你正在投入相当大的力气来将SICP中的想法实现为不同的语言。 还要注意,所有这些都与我之前提到的一点有关:如果你遵循这本书,那么你实现的语言将在词汇范围内,使它远离Common Lisp主机语言。 所以总的来说,你完全失去了本书所谓的“元评估者”中的“通告”属性。 (再一次,这可能不会打扰你,但会损害整体的学习体验)。总而言之, 很less有语言接近于Scheme,能够在语言中实现语言的语义, (例如,不使用eval
)评估器。 事实上,如果你确实使用Common Lisp,那么在我看来,Rainer的第二个build议 – 使用支持TCO的Common Lisp实现 – 是最好的select。 但是,在Common Lisp中,这基本上是一个编译器优化:所以您可能需要(a)知道实现中的旋钮,以便使TCO发生,(b)您需要确保Common Lisp的实现实际上是在做适当的TCO,而不仅仅是优化自我调用(这是非常简单的情况,而不是那么重要),(c)你会希望 TCO的Common Lisp实现可以在不损害debugging的情况下选项(再一次,因为这被认为是Common Lisp中的一个优化,然后打开这个旋钮,编译器也可能会说“我不在乎可debugging性”)。
最后,我的最后一点不是很难克服,但它在概念上是最重要的。 在Scheme中,你有一个统一的规则:标识符有一个值,这是由词汇决定的 – 就是这样 。 这是一个非常简单的语言。 在Common Lisp中,除了有时使用dynamic作用域的历史包袱,有时还使用词法作用域,还有符号有两个不同的值 – 有一个variables出现在expression式头部时使用的函数值,有一个不同的价值,否则使用。 例如,在(foo foo)
, (foo foo)
的两个实例的每一个都被不同地解释 – 第一个是foo
的函数值,第二个是它的variables值。 再次,这是不难解决的 – 有一些你需要知道处理所有这些的构造。 例如,您不需要编写(lambda (x) (funcall xx))
,而需要编写(lambda (x) (funcall xx))
,这使得被调用的函数出现在一个可变的位置,因此相同的值在那里使用; 另一个例子是(map car something)
,你将需要翻译(map #'car something)
(或更准确地说,你将需要使用普通的Lisp相当于car
function的mapcar
)。 还有一件事情,你需要知道的是let
绑定名称的值槽, labels
绑定function槽(和defvar
和defvar
有非常不同的语法)。但所有这一切的概念结果Common Lispers倾向于使用比Schemersless得多的高阶代码,而这一切都是从每种语言中常见的习语,到实现它们的方式。 (例如,许多Common Lisp编译器永远不会优化这个调用: (funcall foo bar)
,而Scheme编译器会像调用任何函数调用expression式一样优化(foo bar)
,因为没有其他方法可以调用函数了。
最后,我会注意到上面提到的许多非常好的flamewar材料:将这些问题抛到公共的Lisp或Scheme论坛(特别是comp.lang.lisp
和comp.lang.scheme
),而你最可能会看到一个长长的线索,人们在这里解释为什么他们的select比另一个好得多,或者为什么某些“所谓的特征”实际上是由语言devise者在当时显然非常醉的时候做出的一个愚蠢的决定等等。问题是这两种语言之间只是差异,最终人们可以在任何一种语言中完成他们的工作。 如果这项工作是“做SICP”的话,那么考虑到从计划的angular度来看,每个问题都会如何。 如果你想学习Common Lisp,那么使用Common Lisp教科书就会让你失望得多。
你已经知道一些Common Lisp了吗? 我认为这就是“Lisp”的意思。 在这种情况下,您可能想使用它而不是Scheme。 如果你也不知道,而且你正在通过SICP学习经验,那么你可能会比较好。 它对新学习者有更好的支持,你不需要从Scheme到Common Lisp的翻译。
有差异; 具体而言,SICP的高度function性风格在Common Lisp中是较为单调的,因为在传递函数时必须引用函数,并使用funcall
调用绑定到variables的函数。
但是,如果您想使用Common Lisp,则可以尝试使用Eli Bendersky的标签SICP下的SICP代码的Common Lisp译文 。
与Common Lisp一起使用SICP是可能且有趣的
您可以使用Common Lisp进行SICP学习,而不会出现太多问题。 本书中使用的Scheme子集不是很复杂。 SICP不使用macros,也不使用延续。 有DELAY和FORCE,可以用Common Lisp写成几行。
对于初学者来说,使用(function foo)
和(funcall foo 1 2 3)
实际上更好(恕我直言!),因为代码在学习函数式编程部分时变得更加清晰。 你可以看到variables和lambda函数被调用/传递的地方。
Common Lisp中的尾部调用优化
只有一个使用Common Lisp的领域有一个缺点: 尾部呼叫优化 (TCO)。 Common Lisp不支持标准的TCO(由于与其他语言的交互不清楚,并不是所有的计算机体系结构都直接支持它(认为JVM),并非所有的编译器都支持它(一些Lisp Machine),所以它会进行一些debugging/跟踪/更难踩踏,…)。
生活有三种方式:
- 希望堆栈不会被吹掉。 坏。
- 使用支持TCO的Common Lisp实现。 有一些。 见下文。
- 使用DOTIMES,DO,LOOP …将循环(和类似的结构)重写成循环(和类似的结构)…
我个人build议2或3。
Common Lisp拥有出色且易于使用的具有TCO支持的编译器(SBCL,LispWorks,Allegro CL,Clozure CL …),以及作为开发环境使用的内置版本或GNU Emacs / SLIME。
为了与SICP一起使用,我推荐SBCL ,因为它默认情况下总是编译,默认情况下TCO支持,编译器捕获了很多编码问题(未声明的variables,错误的参数列表,一堆types错误,…)。 这在学习过程中有很大帮助。 一般确保编译代码,因为Common Lisp解释器通常不支持TCO。
有时编写一个或两个macros可能也有帮助,并提供一些Scheme函数名称,使代码看起来更像Scheme。 例如,你可以在Common Lisp中有一个DEFINEmacros。
对于更高级的用户,有一个旧的Scheme实现(称为伪计划),它应该运行SICP中的大部分代码。
我的build议是:如果你想多走一步并使用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
macros中直接在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?
CL是numberp
-
CL:COND
使用T
而不是else
-
CL:ERROR
使用CL格式的string
我们来为一些函数定义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
macros:
(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 '(* (* xy) (+ x 3)) 'x) (+ (* (* XY) (+ 1 0)) (* (+ (* X 0) (* 1 Y)) (+ X 3)))
在Common Lisp中的SICPstream示例
请参阅SICP 第3.5章中的书本代码 。 我们从上面使用CL添加。
SICP提到了delay
, the-empty-stream
和cons-stream
,但没有实现。 我们在这里提供Common Lisp的一个实现:
(defmacro delay (expression) `(lambda () ,expression)) (defmacro cons-stream (ab) `(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
来传递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
编辑:弥敦道桑德斯的评论是正确的。 自从我上次阅读这本书以来,显然已经有一段时间了,但我只是查了一下,并没有直接使用call/cc
。 我赞成Nathan的回答。
无论您使用什么,都需要执行SICP使用的延续。 即使所有的Scheme解释器都没有实现它们,我也不知道任何Common Lisp。
他们是相似的,但不一样。
我相信如果你和Scheme一起使用,会更容易。
我会select像Clojure这样更实用的方言,Clojure运行在JVM上,可以使用所有Java库,这是一个巨大的优势。 Clojure也是现代的Lisp,并且拥有很好的概念。 它有不断壮大的社区。 如果你想尝试Clojure,我会build议你Clojurecademy这是一个交互式的基于Clojure的课程平台,我为新来者创build。