¿En qué se diferencia el ciclo de lectura-evaluación-impresión de Lisp del de Python?

Me he encontrado con una siguiente statement de Richard Stallman :

‘Cuando inicia un sistema Lisp, ingresa en un ciclo de lectura-evaluación-impresión. La mayoría de los otros idiomas no tienen nada comparable a leer, nada comparable a eval, y nada comparable a imprimir. ¡Qué enormes deficiencias! ‘

Ahora, hice muy poca progtwigción en Lisp, pero escribí una cantidad considerable de código en Python y recientemente un poco en Erlang. Mi impresión fue que estos lenguajes también ofrecen un ciclo de lectura-evaluación-impresión, pero Stallman no está de acuerdo (al menos sobre Python):

“Revisé la documentación de Python cuando la gente me dijo que era fundamentalmente similar a Lisp. Mi conclusión es que eso no es así. Cuando inicias Lisp, hace ‘leer’, ‘eval’ e ‘imprimir’, todos faltan en Python ‘.

¿Existe realmente una diferencia técnica fundamental entre los ciclos de lectura y evaluación de Lisp y Python? ¿Puede dar ejemplos de cosas que el REPL de Lisp facilita y que son difíciles de hacer en Python?

En apoyo de la posición de Stallman, Python no hace lo mismo que los sistemas Lisp típicos en las siguientes áreas:

  • La función de read en Lisp lee una expresión S, que representa una estructura de datos arbitraria que puede tratarse como datos o evaluarse como código. Lo más cercano en Python lee una sola cadena, que tendrías que analizarte a ti mismo si quieres que signifique algo.

  • La función eval en Lisp puede ejecutar cualquier código Lisp. La función eval en Python solo evalúa expresiones y necesita la sentencia exec para ejecutar sentencias. Pero ambos funcionan con el código fuente de Python representado como texto, y tienes que saltar a través de un montón de aros para “evaluar” un AST de Python.

  • La función de print en Lisp escribe una expresión en S exactamente de la misma forma que la read acepta. print en Python imprime algo definido por los datos que intenta imprimir, que ciertamente no siempre es reversible.

La statement de Stallman es un poco falsa, porque claramente Python tiene funciones llamadas exactamente eval e print , pero hacen algo diferente (e inferior) a lo que él espera.

En mi opinión, Python tiene algunos aspectos similares a Lisp, y puedo entender por qué la gente podría haber recomendado que Stallman investigue Python. Sin embargo, como argumenta Paul Graham en What Made Lisp Different , cualquier lenguaje de progtwigción que incluya todas las capacidades de Lisp, también debe ser Lisp.

El punto de Stallman es que no implementar un “lector” explícito hace que el REPL de Python parezca paralizado en comparación con Lisps porque elimina un paso crucial del proceso REPL. Reader es el componente que transforma un flujo de entrada de texto en la memoria; piense en algo así como un analizador XML integrado en el lenguaje y utilizado tanto para el código fuente como para los datos. Esto es útil no solo para escribir macros (lo que en teoría sería posible en Python con el módulo ast ), sino también para la depuración y la introspección.

Digamos que está interesado en cómo se implementa el formulario especial de incf . Puedes probarlo así:

 [4]> (macroexpand '(incf a)) (SETQ A (+ A 1)) ; 

Pero incf puede hacer mucho más que incrementar los valores de los símbolos. ¿Qué hace exactamente cuando se le pide que incremente una entrada de tabla hash? Veamos:

 [2]> (macroexpand '(incf (gethash htable key))) (LET* ((#:G3069 HTABLE) (#:G3070 KEY) (#:G3071 (+ (GETHASH #:G3069 #:G3070) 1))) (SYSTEM::PUTHASH #:G3069 #:G3070 #:G3071)) ; 

Aquí aprendemos que incf llama a una función puthash específica del puthash , que es un detalle de implementación de este sistema Common Lisp. Observe cómo la “impresora” utiliza funciones conocidas por el “lector”, como la introducción de símbolos anónimos con la syntax #: y la referencia a los mismos símbolos dentro del scope de la expresión expandida. Emular este tipo de inspección en Python sería mucho más detallado y menos accesible.

Además de los usos obvios en el REPL, los experimentados usuarios de Lisper print y read el código como una herramienta de serialización simple y fácilmente disponible, comparable a XML o json. Si bien Python tiene la función str , equivalente a la print de Lisp, carece del equivalente de read , el equivalente más cercano es eval . eval supuesto, eval combina dos conceptos diferentes, el análisis y la evaluación, lo que conduce a problemas como este y soluciones como esta y es un tema recurrente en los foros de Python. Esto no sería un problema en Lisp precisamente porque el lector y el evaluador están claramente separados.

Finalmente, las características avanzadas de la facilidad de lectura le permiten al progtwigdor extender el lenguaje de maneras que ni siquiera las macros podrían proporcionar. Un ejemplo perfecto de cómo hacer posible las cosas difíciles es el paquete de infix de Mark Kantrowitz, que implementa una syntax de infijo con todas las funciones como una macro de lector.

En un sistema basado en Lisp, uno típicamente desarrolla el progtwig mientras se ejecuta desde el REPL (lea el ciclo de impresión eval). Así que integra un montón de herramientas: finalización, editor, intérprete de línea de comandos, depurador, … Lo predeterminado es tener eso. Escriba una expresión con un error: se encuentra en otro nivel de REPL con algunos comandos de depuración habilitados. Realmente tienes que hacer algo para deshacerte de este comportamiento.

Puedes tener dos significados diferentes del concepto REPL:

  • el ciclo de lectura Read Print Print como en Lisp (o en algunos otros lenguajes similares). Lee progtwigs y datos, evalúa e imprime los datos de resultados. Python no funciona de esta manera. El REPL de Lisp le permite trabajar directamente en una meta-progtwigción, escribiendo el código que genera (código), verificando las expansiones, transformando el código real, etc. Lisp ha leído / eval / print como el bucle superior. Python tiene algo como leer / evaluar / imprimir cadena como el bucle superior.

  • la interfaz de línea de comandos. Una shell interactiva. Ver por ejemplo para IPython . Compara eso con SLIME de Common Lisp.

El shell predeterminado de Python en el modo predeterminado no es realmente tan poderoso para el uso interactivo:

 Python 2.7.2 (default, Jun 20 2012, 16:23:33) [GCC 4.2.1 Compatible Apple Clang 4.0 (tags/Apple/clang-418.0.60)] on darwin Type "help", "copyright", "credits" or "license" for more information. >>> a+2 Traceback (most recent call last): File "", line 1, in  NameError: name 'a' is not defined >>> 

Recibes un mensaje de error y eso es todo.

Compare eso con el CLISP REPL:

 rjmba:~ joswig$ clisp iiiiiii ooooo o ooooooo ooooo ooooo IIIIIII 8 8 8 8 8 o 8 8 I \ `+' / I 8 8 8 8 8 8 \ `-+-' / 8 8 8 ooooo 8oooo `-__|__-' 8 8 8 8 8 | 8 o 8 8 o 8 8 ------+------ ooooo 8oooooo ooo8ooo ooooo 8 Welcome to GNU CLISP 2.49 (2010-07-07)  Copyright (c) Bruno Haible, Michael Stoll 1992, 1993 Copyright (c) Bruno Haible, Marcus Daniels 1994-1997 Copyright (c) Bruno Haible, Pierpaolo Bernardi, Sam Steingold 1998 Copyright (c) Bruno Haible, Sam Steingold 1999-2000 Copyright (c) Sam Steingold, Bruno Haible 2001-2010 Type :h and hit Enter for context help. [1]> (+ a 2) *** - SYSTEM::READ-EVAL-PRINT: variable A has no value The following restarts are available: USE-VALUE :R1 Input a value to be used instead of A. STORE-VALUE :R2 Input a new value for A. ABORT :R3 Abort main loop Break 1 [2]> 

CLISP usa el sistema de condiciones de Lisp para entrar en un depurador REPL. Presenta algunos reinicios. Dentro del contexto de error, el nuevo REPL proporciona comandos extendidos.

Vamos a usar el :R1 reinicio de :R1 :

 Break 1 [2]> :r1 Use instead of A> 2 4 [3]> 

De esta forma se obtiene la reparación interactiva de progtwigs y ejecuciones ejecutadas …

El modo interactivo de Python difiere del modo de “leer código de archivo” de Python en varias formas pequeñas y cruciales, probablemente inherentes a la representación textual del lenguaje. Python tampoco es homoicónico, algo que me hace llamarlo “modo interactivo” en lugar de “ciclo de lectura-eval-print”. Aparte de eso, diría que es más una diferencia de grado que una diferencia de tipo.

Ahora, algo que se acerca a la “diferencia en especie”, en un archivo de código de Python, puede insertar fácilmente líneas en blanco:

 def foo(n): m = n + 1 return m 

Si intenta pegar el código idéntico en el intérprete, considerará que la función está “cerrada” y se quejará de que tiene una statement de devolución desnuda en la sangría incorrecta. Esto no sucede en (Common) Lisp.

Además, hay algunas variables de conveniencia bastante útiles en Common Lisp (CL) que no están disponibles (al menos que yo sepa) en Python. Tanto CL como Python tienen “valor de la última expresión” ( * en CL, _ en Python), pero CL también tiene ** (valor de expresión antes de la última) y *** (el valor de la anterior) y + , ++ y +++ (las propias expresiones). CL tampoco distingue entre expresiones y declaraciones (en esencia, todo es una expresión) y todo eso ayuda a construir una experiencia REPL mucho más rica.

Como dije al principio, es más una diferencia en el grado que una diferencia en la clase. Pero si la brecha hubiera sido solo un poco más amplia entre ellos, probablemente también sería una diferencia de tipo.