2009-07-21 12 views

Respuesta

1

Son similares pero no iguales.

Creo que si vas con Scheme sería más fácil.

2

Editar: El comentario de Nathan Sanders es correcto. Claramente ha pasado un tiempo desde la última vez que leí el libro, pero acabo de verificarlo y no usa call/cc directamente. He votado a favor la respuesta de Nathan.


Cualquier cosa que use necesita implementar implementaciones, que SICP usa mucho. Ni siquiera todos los intérpretes de Scheme los implementan, y no conozco ningún Common Lisp que lo haga.

+1

Common Lisp puede tener continuaciones escritas encima. Weblocks es una de esas aplicaciones que utiliza las continuaciones para escribir aplicaciones web. –

+3

http://common-lisp.net/project/cl-cont/ – AraK

+0

Entonces, básicamente necesito usar Scheme. ¿Qué implementación recomiendas? Solo he intentado PLT Scheme. – akway

11

¿Ya conoces algunos Common Lisp? Supongo que eso es lo que quieres decir con 'Lisp'. En ese caso, es posible que desee usarlo en lugar de Scheme. Si usted no sabe tampoco, y está trabajando a través del SICP únicamente para la experiencia de aprendizaje, entonces probablemente esté mejor con Scheme. Tiene mucho mejor soporte para nuevos estudiantes, y no tendrá que traducir de Scheme a Common Lisp.

Existen diferencias; específicamente, el estilo altamente funcional de SICP es más complejo en Common Lisp porque tiene que citar funciones al pasarlas y usar funcall para llamar a una función vinculada a una variable.

Sin embargo, si desea usar Common Lisp, puede intentar usar Eli Bendersky's Common Lisp translations of the SICP code bajo la etiqueta SICP.

+0

El enlace está muerto :( –

+3

@ Reb.Cabin http://eli.thegreenplace.net/2007/06/19/introducing-the-sicp-reading-notes y más enlaces bajo la etiqueta SICP http://eli.thegreenplace.net/tag/sicp. –

10

Usando SICP con Common Lisp es posible y divertido

Puede utilizar Common Lisp para el aprendizaje con SICP sin muchos problemas. El subconjunto de Scheme que se usa en el libro no es muy sofisticado. SICP no usa macros y no usa continuaciones. Hay DELAY y FORCE, que se pueden escribir en Common Lisp en unas pocas líneas.

También para un principiante que usa (function foo) y (funcall foo 1 2 3) es realmente mejor (¡En mi humilde opinión!), Porque el código se vuelve más claro al aprender las partes de programación funcionales. Puede ver dónde se llaman/pasan las variables y las funciones lambda.

optimización de la llamada del extremo en Common Lisp

Sólo hay un área grande donde el uso de Common Lisp tiene un inconveniente: cola de optimización llamada (TCO). Common Lisp no admite TCO en su estándar (debido a una interacción poco clara con el resto del lenguaje, no todas las arquitecturas informáticas lo soportan directamente (piense en JVM), no todos los compiladores lo soportan (algunas máquinas Lisp), realiza alguna depuración/rastreo/pisando más fuerte, ...).

Hay tres maneras de vivir con eso:

  1. la esperanza de que la pila no sopla hacia fuera. MALO.
  2. Utilice una implementación Common Lisp que admita TCO. Hay algunos. Vea abajo.
  3. Reescriba los bucles funcionales (y construcciones similares) en bucles (y construcciones similares) utilizando DOTIMES, DO, LOOP, ...

Personalmente recomendaría 2 o 3.

Common Lisp tiene excelentes y fácil de usar compiladores con soporte TCO (SBCL, LispWorks, Allegro CL, CL Clozure, ...) y como un entorno de desarrollo use cualquiera de los incorporados o GNU Emacs/SLIME.

Para utilizar con SICP recomendaría SBCL, ya que compila siempre de forma predeterminada, tiene soporte TCO por defecto y el compilador detecta muchos problemas de codificación (variables no declaradas, listas de argumentos incorrectos, un montón de errores de tipo, ... .). Esto ayuda mucho durante el aprendizaje. Por lo general, asegúrese de que el código esté compilado, ya que los intérpretes de Common Lisp generalmente no admiten el TCO.

A veces también puede ser útil escribir una o dos macros y proporcionar algunos nombres de funciones de esquema para hacer que el código se parezca un poco más a Scheme. Por ejemplo, podría tener una macro DEFINE en Common Lisp.

Para los usuarios más avanzados, existe una antigua implementación de Scheme (llamada Pseudo Scheme), que debería ejecutar la mayor parte del código en SICP.

Mi recomendación: si quiere hacer un esfuerzo adicional y usar Common Lisp, hágalo.

Para que sea más fácil de entender los cambios necesarios, he añadido algunos ejemplos - Recuerdo, que necesita un compilador Common Lisp con soporte para la optimización de llamada cola :

Ejemplo

Veamos este código simple de 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))) 

podemos usarlo directamente en Common Lisp con un mac DEFINE ro:

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

Ahora debe usar SBCL, CCL, Allegro CL o LispWorks. Estos compiladores admiten TCO de forma predeterminada.

Vamos a usar 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.... 

Otro ejemplo: la diferenciación simbólica

SICP tiene un ejemplo de esquema de diferenciación:

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

Hacer el código de ejecución en Common Lisp es fácil :

  • algunas funciones tienen diferentes nombres, number? es numberp en CL
  • CL:COND utiliza T en lugar de else
  • CL:ERROR utiliza cadenas de formato CL

vamos a definir nombres de esquema para algunas funciones.código de 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))) 

Nuestra define macro desde arriba:

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

El código de 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)))) 

Vamos a intentarlo en LispWorks:

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

Ejemplo de secuencias desde SICP en Common Lisp

Consulte book code in chapter 3.5 en SICP. Usamos las adiciones a CL desde arriba.

SICP menciona delay, the-empty-stream y cons-stream, pero no lo implementa. Ofrecemos aquí una implementación en 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")) 

Ahora viene código portable del libro:

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

Ahora Common Lisp difiere en stream-for-each:

  • tenemos que utilizar cl:progn vez de begin
  • parámetros de la función deben llamarse con cl:funcall

Aquí es una versión:

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

También tenemos que pasar a funciones usando cl:function:

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

Pero entonces el ejemplo funciona:

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 
106

usted tiene varias respuestas aquí, pero ninguna es realmente completa (y estoy n O hablando de tener suficientes detalles o ser lo suficientemente largo). En primer lugar, la conclusión: debe no utilizar Common Lisp si desea tener una buena experiencia con SICP.

Si no sabes mucho Common Lisp, solo tómalo como eso. (Obviamente, puede ignorar este consejo como cualquier otra cosa, algunas personas solo aprenden de la manera difícil)

Si ya conoce Common Lisp, entonces puede llevarlo a cabo, pero con un esfuerzo considerable y un daño considerable a su experiencia de aprendizaje general. Hay algunos problemas fundamentales que separan Common Lisp y Scheme, que hacen que tratar de usar el anterior con SICP sea una idea bastante mala. De hecho, si tiene el nivel de conocimiento para hacerlo funcionar, entonces es probable que esté por encima del nivel de SICP de todos modos. No digo que no sea posible; por supuesto, es posible implementar todo el libro en Common Lisp (por ejemplo, ver las páginas de Bendersky) del mismo modo que lo puedes hacer en C, Perl o lo que sea. Simplemente va a ser más difícil con idiomas que están más allá de Scheme.(Por ejemplo, es probable que ML sea más fácil de usar que Common Lisp, incluso cuando su sintaxis es muy diferente.)

A continuación, se incluyen algunos de estos problemas importantes, en orden creciente de importancia. (No estoy diciendo que esta lista es exhaustiva de ninguna manera, estoy seguro de que hay un montón de cuestiones adicionales que estoy omitiendo aquí.)

  1. NIL y asuntos relacionados, y diferente nombres.

  2. Ámbito dinámico.

  3. Optimización de la llamada de la cola.

  4. Separar el espacio de nombres para funciones y valores.

Voy a ampliar ahora en cada uno de estos puntos:

El primer punto es el más técnico. En Common Lisp, NIL se usa como la lista vacía y como el valor falso. En sí mismo, este no es un gran problema, y ​​de hecho, la primera edición del SICP tenía una suposición similar, donde la lista vacía y la falsa tenían el mismo valor. Sin embargo, el NIL de Common Lisp todavía es diferente: también es un símbolo. Entonces, en Scheme tienes una clara separación: algo es una lista, o uno de los tipos primitivos de valores, pero en Common Lisp, NIL no es solo falso y la lista vacía: es también un símbolo. Además de esto, obtienes un host de comportamiento ligeramente diferente: por ejemplo, en Common Lisp, la cabeza y la cola (car y cdr) de la lista vacía es en sí misma la lista vacía, mientras que en Scheme obtendrás un error de tiempo de ejecución si intenta eso. Para colmo, tiene diferentes nombres y convenciones de nombres, por ejemplo, predicados en Common Lisp finalizados por convención con P (por ejemplo, listp) mientras que los predicados en Scheme terminan en un signo de interrogación (por ejemplo, list?); los mutadores en Common Lisp no tienen una convención específica (algunos tienen un prefijo N), mientras que en Scheme casi siempre tienen un sufijo de !. Además, la asignación simple en Common Lisp es generalmentesetf y también puede operar en combinaciones (por ejemplo, (setf (car foo) 1)), mientras que en Scheme es set! y se limita a establecer variables vinculadas solamente. (Tenga en cuenta que Common Lisp también tiene la versión limitada, se llama setq. Casi nadie lo usa)

El segundo punto es mucho más profundo, y posiblemente uno que conducirá a un comportamiento completamente incomprensible de su código. El hecho es que en Common Lisp, los argumentos de función tienen un alcance léxico, pero las variables que se declaran con defvar son dinámicamente con ámbito. Existe una amplia gama de soluciones que se basan en enlaces de ámbito léxico, y en Common Lisp simplemente no funcionan. Por supuesto, el hecho de que Common Lisp tenga alcance léxico significa que puede evitar esto teniendo mucho cuidado con las nuevas vinculaciones, y posiblemente usando macros para evitar el alcance dinámico predeterminado, pero nuevamente, esto requiere una extensión mucho más extensa. conocimiento que tiene un novato típico. Las cosas empeoran aún más: si declara un nombre específico con un defvar, ese nombre se vinculará dinámicamente incluso si son argumentos para las funciones. Esto puede llevar a algunos errores extremadamente difíciles de rastrear que se manifiestan de una manera extremadamente confusa (básicamente obtienes el valor incorrecto, y no tendrás idea de por qué sucede eso).Los Common Lispers experimentados lo saben (especialmente los que han sido quemados), y siempre siguen la convención de usar estrellas con nombres de ámbito dinámico (p. Ej., *foo*). (Y, por cierto, en la jerga de Common Lisp, estas variables de ámbito dinámico se llaman simplemente "variables especiales", que es otra fuente de confusión para los novatos.)

El tercer punto también se discutió en algunos de los comentarios anteriores . De hecho, Rainer tuvo un buen resumen de las diferentes opciones que tiene, pero no explicó qué tan difícil puede hacer las cosas. Lo que pasa es que uno de los conceptos fundamentales de Scheme es la adecuada optimización de llamadas de cola (TCO). Es lo suficientemente importante que es un lenguaje función en lugar de simplemente una optimización. Un bucle típico en Scheme se expresa como una función de cola de cola (por ejemplo, (define (loop) (loop))) y las implementaciones de Scheme adecuadas son requiere para implementar TCO lo que garantizará que esto sea, de hecho, un bucle infinito en lugar de ejecutarse durante un tiempo breve hasta que explotes el espacio de la pila. Esta es toda la esencia de la primera solución de Rainer, y la razón por la que lo etiquetó como "MALO". Su tercera opción, la reescritura de bucles funcionales (expresados ​​como funciones recursivas) como bucles Common Lisp (dotimes, dolist y el infame loop) puede funcionar por unos pocos casos simples, pero a un costo muy elevado: el hecho de que Scheme es el lenguaje que hace un TCO adecuado no solo es fundamental para el idioma; también es uno de los temas principales del libro, por lo que al hacerlo perderás ese punto por completo. Además, hay algunos casos en los que solo no se puede traducir el código de Scheme en una construcción de bucle de Lisp común; por ejemplo, a medida que avance en el libro, podrá implementar un intérprete de metacirculación que es una implementación de un lenguaje mini-Scheme. Se necesita un cierto clic para darnos cuenta de que este metaevaluador implementa un lenguaje que a su vez está haciendo TCO si el lenguaje en el que implementa este evaluador está haciendo TCO en sí. (Tenga en cuenta que estoy hablando de los intérpretes "simples"; más adelante en el libro, implementará este evaluador como algo cercano a una máquina de registro, donde explícitamente hará que haga TCO). La conclusión de todo esto, es que este evaluador, cuando se implemente en Common Lisp, dará como resultado un lenguaje que no está haciendo TCO por sí mismo. Las personas que están familiarizadas con todo esto no deberían sorprenderse: después de todo, la "circularidad" del evaluador significa que está implementando un lenguaje con semántica que está muy cerca del idioma del sistema principal, por lo que en este caso "hereda". "la semántica Common Lisp en lugar de la semántica Scheme TCO. Sin embargo, esto significa que su mini-evaluador está ahora lisiado: no tiene TCO, ¡así que no tiene manera de hacer bucles! Para obtener bucles, deberá implementar nuevas construcciones en su intérprete, que generalmente usará las construcciones de iteración en Common Lisp. Pero ahora se está alejando de lo que está en el libro, y está invirtiendo un esfuerzo considerable en aproximadamente implementando las ideas en SICP para el idioma diferente. Tenga en cuenta también que todo esto está relacionado con el punto anterior que planteé: si sigue el libro, entonces el lenguaje que implemente tendrá un alcance léxico, alejándolo del lenguaje de host Common Lisp. Entonces, en general, pierde por completo la propiedad "circular" en lo que el libro llama "evaluador meta circular". (De nuevo, esto es algo que puede no molestarle, pero dañará la experiencia de aprendizaje en general.) En general, muy pocos idiomas se acercan a Scheme al poder implementar la semántica del lenguaje dentro del lenguaje como un no trivial (por ejemplo, no usando eval) evaluador que fácilmente. De hecho, si vas con un Lisp común, entonces, en mi opinión, la segunda sugerencia de Rainer (utilizar una implementación de Common Lisp que admita TCO) es la mejor manera de hacerlo.Sin embargo, en Common Lisp esto es fundamentalmente una optimización del compilador: por lo que es probable que necesite (a) conocer las perillas en la implementación que necesita activar para que ocurra el TCO, (b) deberá asegurarse de que el Común La implementación de Lisp está haciendo un TCO correcto, y no solo la optimización de self llamadas (que es el caso mucho más simple que no es tan importante), (c) espero que la implementación de Common Lisp que hace TCO puede hacer así que sin dañar las opciones de depuración (una vez más, dado que esto se considera una optimización en Common Lisp, al encenderlo, el compilador también podría interpretarlo diciendo "No me importa mucho la depuración").

Finalmente, mi último punto no es demasiado difícil de superar, pero conceptualmente es el más importante. En Scheme, tiene una regla uniforme: los identificadores tienen un valor, que se determina léxicamente, y eso es todo. Es un lenguaje muy simple. En Common Lisp, además del bagaje histórico de utilizar el alcance dinámico y, a veces, usar el alcance léxico, tiene símbolos que tienen dos valor diferente - existe el valor de la función que se usa cada vez que aparece una variable en el encabezado de una expresión , y hay un valor diferente que se utiliza de otro modo. Por ejemplo, en (foo foo), cada una de las dos instancias de foo se interpretan de manera diferente: la primera es el valor de la función foo y la segunda es su valor variable. Nuevamente, esto no es difícil de superar: hay una serie de construcciones que debe conocer para manejar todo esto. Por ejemplo, en lugar de escribir (lambda (x) (x x)), debe escribir (lambda (x) (funcall x x)), lo que hace que la función que se está llamando aparezca en una posición variable, por lo tanto, se usará el mismo valor allí; otro ejemplo es (map car something) que necesitará traducir a (map #'car something) (o más exactamente, necesitará usar mapcar que es el equivalente de Common Lisp de la función car); otra cosa que necesitarás saber es que let enlaza la ranura del valor del nombre, y labels enlaza la ranura de la función (y tiene una sintaxis muy diferente, como defun y defvar.) Pero el resultado conceptual de todos esto es que Common Lispers tiende a usar código de orden superior mucho menos que Schemers, y eso va desde los modismos que son comunes en cada idioma, hasta lo que las implementaciones harán con él. (Por ejemplo, muchos compiladores Common Lisp nunca optimizar esta llamada:. (funcall foo bar), mientras que los compiladores Esquema optimizarán (foo bar) como cualquier expresión de llamada a la función, porque no hay ningún otro forma de llamar a las funciones)

Por último, me quedo Tenga en cuenta que gran parte de lo anterior es un material muy bueno: lanzar cualquiera de estos problemas en un foro público de Lisp o Scheme (en particular comp.lang.lisp y comp.lang.scheme), y lo más probable es que vea un hilo largo donde la gente explique por qué su elección está lejos mejor que el otro, o por qué alguna "característica llamada" es en realidad una decisión idiota hecha por diseñadores de lenguaje que estaban claramente muy borrachos en ese momento, etc. etc. Pero lo cierto es que estas son solo diferencias entre los dos idiomas, y eventua Sólo la gente puede hacer su trabajo en cualquiera de los dos. Simplemente sucede que si el trabajo es "hacer SICP" entonces Scheme será mucho más fácil considerando cómo aborda cada uno de estos problemas desde la perspectiva del Esquema. Si quieres aprender Common Lisp, ir con un libro de texto de Common Lisp te dejará mucho menos frustrado.

+3

Lo leí de una sola vez, hiciste un buen trabajo explicando, y va al grano. No estoy tan familiarizado con Scheme, y ciertas aclaraciones que hiciste facilitaron mi transición de CL mientras aprendías Scheme. –

+0

¿Podrías dar una referencia al TCO? de Scheme? ¿Tiene algo así como http://www.r6rs.org/final/html/r6rs/r6rs-ZH-14.html#node_sec_11.20 en mente? –

+2

Sí, todos los esquemas son necesarios para realizar la optimización de la cola de llamada . Es un requisito tan fundamental que algunas personas No nos gusta utilizar la "optimización" con el argumento de que es una característica real, y no solo algo para acelerar las cosas, por lo que un término más nuevo es la eliminación de las llamadas finales. –

0

Me gustaría ir con un dialecto más práctico como Clojure, Clojure se ejecuta en JVM y puede consumir todas las bibliotecas de Java, lo que es una gran ventaja. También Clojure es un Lisp moderno y abarca conceptos agradables. Tiene una comunidad creciente y agradable. Si quieres probar Clojure, te sugiero Clojurecademy, que es una plataforma interactiva de cursos basada en Clojure que he creado para los recién llegados.

Cuestiones relacionadas