Can I use Common Lisp for SICP or is Scheme the only option?

前端 未结 5 862
一个人的身影
一个人的身影 2021-01-29 22:55

Also, even if I can use Common Lisp, should I? Is Scheme better?

5条回答
  •  余生分开走
    2021-01-29 23:43

    Using SICP with Common Lisp is possible and fun

    You can use Common Lisp for learning with SICP without much problems. The Scheme subset that is used in the book is not very sophisticated. SICP does not use macros and it uses no continuations. There are DELAY and FORCE, which can be written in Common Lisp in a few lines.

    Also for a beginner using (function foo) and (funcall foo 1 2 3) is actually better (IMHO !), because the code gets clearer when learning the functional programming parts. You can see where variables and lambda functions are being called/passed.

    Tail call optimization in Common Lisp

    There is only one big area where using Common Lisp has a drawback: tail call optimization (TCO). Common Lisp does not support TCO in its standard (because of unclear interaction with the rest of the language, not all computer architectures support it directly (think JVM), not all compilers support it (some Lisp Machine), it makes some debugging/tracing/stepping harder, ...).

    There are three ways to live with that:

    1. Hope that the stack does not blow out. BAD.
    2. Use a Common Lisp implementation that supports TCO. There are some. See below.
    3. Rewrite the functional loops (and similar constructs) into loops (and similar constructs) using DOTIMES, DO, LOOP, ...

    Personally I would recommend 2 or 3.

    Common Lisp has excellent and easy to use compilers with TCO support (SBCL, LispWorks, Allegro CL, Clozure CL, ...) and as a development environment use either the built-in ones or GNU Emacs/SLIME.

    For use with SICP I would recommend SBCL, since it compiles always by default, has TCO support by default and the compiler catches a lot of coding problems (undeclared variables, wrong argument lists, a bunch of type errors, ...). This helps a lot during learning. Generally make sure the code is compiled, since Common Lisp interpreters will usually not support TCO.

    Sometimes it might also helpful to write one or two macros and provide some Scheme function names to make code look a bit more like Scheme. For example you could have a DEFINE macro in Common Lisp.

    For the more advanced users, there is an old Scheme implementation written in Common Lisp (called Pseudo Scheme), that should run most of the code in SICP.

    My recommendation: if you want to go the extra mile and use Common Lisp, do it.

    To make it easier to understand the necessary changes, I've added a few examples - remember, it needs a Common Lisp compiler with support for tail call optimization:

    Example

    Let's look at this simple code from 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)))
    

    We can use it directly in Common Lisp with a DEFINE macro:

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

    Now you should use SBCL, CCL, Allegro CL or LispWorks. These compilers support TCO by default.

    Let's use 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....
    

    Another Example: symbolic differentiation

    SICP has a Scheme example for differentiation:

    (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))))
    

    Making this code run in Common Lisp is easy:

    • some functions have different names, number? is numberp in CL
    • CL:COND uses T instead of else
    • CL:ERROR uses CL format strings

    Let's define Scheme names for some functions. Common Lisp code:

    (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)))
    

    Our define macro from above:

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

    The Common Lisp code:

    (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))))
    

    Let's try it in LispWorks:

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

    Streams example from SICP in Common Lisp

    See the book code in chapter 3.5 in SICP. We use the additions to CL from above.

    SICP mentions delay, the-empty-stream and cons-stream, but does not implement it. We provide here an implementation in 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"))
    

    Now comes portable code from the book:

    (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))))
    

    Now Common Lisp differs in stream-for-each:

    • we need to use cl:progn instead of begin
    • function parameters need to be called with cl:funcall

    Here is a version:

    (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)))))
    

    We also need to pass functions using cl:function:

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

    But then the example works:

    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
    

提交回复
热议问题