将 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 编写的旧 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? 在 CL 中是 numberp
-
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 中的book code in chapter 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