¿Por qué es exactamente eval evil?

141

Sé que los programadores de Lisp y Scheme suelen decir que se evaldebe evitar a menos que sea estrictamente necesario. He visto la misma recomendación para varios lenguajes de programación, pero aún no he visto una lista de argumentos claros en contra del uso de eval. ¿Dónde puedo encontrar una cuenta de los posibles problemas de uso eval?

Por ejemplo, conozco los problemas de la GOTOprogramación de procedimientos (hace que los programas sean ilegibles y difíciles de mantener, los problemas de seguridad sean difíciles de encontrar, etc.), pero nunca he visto los argumentos en contra eval.

Curiosamente, los mismos argumentos en contra GOTOdeberían ser válidos en contra de las continuaciones, pero veo que Schemers, por ejemplo, no dirá que las continuaciones son "malvadas" - solo debes tener cuidado al usarlas. Es mucho más probable que frunzan el ceño al usar código evalque con el uso de continuaciones (por lo que puedo ver, podría estar equivocado).

Arrendajo
fuente
55
eval no es malo, pero el mal es lo que evalúa
Anurag
9
@yar: creo que tu comentario indica una visión del mundo muy centrada en objetos. Probablemente sea válido para la mayoría de los idiomas, pero sería diferente en Common Lisp, donde los métodos no pertenecen a las clases e incluso más diferente en Clojure, donde las clases solo son compatibles con las funciones de interoperabilidad de Java. Jay etiquetó esta pregunta como Scheme, que no tiene ninguna noción incorporada de clases o métodos (varias formas de OO están disponibles como bibliotecas).
Zak
3
@Zak, tienes razón, solo conozco los idiomas que conozco, pero incluso si estás trabajando con un documento de Word sin usar Estilos, no estás SECO. Mi punto era utilizar la tecnología para no repetirse. OO no es universal, cierto ...
Dan Rosenstark
44
Me tomé la libertad de agregar la etiqueta clojure a esta pregunta, ya que creo que los usuarios de Clojure podrían beneficiarse de la exposición a las excelentes respuestas publicadas aquí.
Michał Marczyk el
... bueno, para Clojure, se aplica al menos un motivo adicional: pierde la compatibilidad con ClojureScript y sus derivados.
Charles Duffy el

Respuestas:

148

Hay varias razones por las cuales uno no debe usar EVAL.

La razón principal para los principiantes es: no lo necesitas.

Ejemplo (suponiendo Common Lisp):

EVALUAR una expresión con diferentes operadores:

(let ((ops '(+ *)))
  (dolist (op ops)
    (print (eval (list op 1 2 3)))))

Eso está mejor escrito como:

(let ((ops '(+ *)))
  (dolist (op ops)
    (print (funcall op 1 2 3))))

Hay muchos ejemplos en los que los principiantes que aprenden Lisp piensan que necesitan EVAL, pero no lo necesitan, ya que las expresiones se evalúan y también se puede evaluar la parte de la función. La mayoría de las veces el uso de EVALmuestra una falta de comprensión del evaluador.

Es el mismo problema con las macros. A menudo, los principiantes escriben macros, donde deben escribir funciones, sin entender para qué son realmente las macros y sin entender que una función ya hace el trabajo.

A menudo es la herramienta incorrecta para el trabajo EVALy a menudo indica que el principiante no comprende las reglas habituales de evaluación de Lisp.

Si cree que lo necesita EVAL, verifique si algo como FUNCALL, REDUCEo APPLYpodría usarse en su lugar.

  • FUNCALL - llamar a una función con argumentos: (funcall '+ 1 2 3)
  • REDUCE - llame a una función en una lista de valores y combine los resultados: (reduce '+ '(1 2 3))
  • APPLY- llamar a una función con una lista como los argumentos: (apply '+ '(1 2 3)).

P: ¿realmente necesito evaluar o el compilador / evaluador ya es lo que realmente quiero?

Las principales razones a evitar EVALpara usuarios un poco más avanzados:

  • desea asegurarse de que su código esté compilado, porque el compilador puede verificar el código en busca de muchos problemas y genera código más rápido, a veces MUCHO MUCHO (factor 1000 ;-)) código más rápido

  • el código que se construye y necesita ser evaluado no se puede compilar lo antes posible.

  • la evaluación de la entrada arbitraria del usuario abre problemas de seguridad

  • cierto uso de la evaluación EVALpuede ocurrir en el momento equivocado y crear problemas de compilación

Para explicar el último punto con un ejemplo simplificado:

(defmacro foo (a b)
  (list (if (eql a 3) 'sin 'cos) b))

Por lo tanto, es posible que desee escribir una macro que, según el primer parámetro, use SINo COS.

(foo 3 4)hace (sin 4)y (foo 1 4)hace (cos 4).

Ahora podemos tener:

(foo (+ 2 1) 4)

Esto no da el resultado deseado.

Entonces, uno puede querer reparar la macro FOOEVALUANDO la variable:

(defmacro foo (a b)
  (list (if (eql (eval a) 3) 'sin 'cos) b))

(foo (+ 2 1) 4)

Pero esto todavía no funciona:

(defun bar (a b)
  (foo a b))

El valor de la variable simplemente no se conoce en tiempo de compilación.

Una razón general importante para evitar EVAL: a menudo se usa para hacks feos.

Rainer Joswig
fuente
3
¡Gracias! Simplemente no entendí el último punto (¿evaluación en el momento equivocado?), ¿Podría explicarnos un poco, por favor?
Jay
41
+1 ya que esta es la respuesta real: las personas recurren evalsimplemente porque no saben que hay un idioma específico o una función de biblioteca para hacer lo que quieren hacer. Ejemplo similar de JS: Quiero obtener una propiedad de un objeto usando un nombre dinámico, así que escribo: eval("obj.+" + propName)cuando podría haber escrito obj[propName].
Daniel Earwicker
¡Ahora entiendo a qué te refieres, Rainer! Thansk!
Jay
@Daniel "obj.+":? Lo último que verifiqué, +no es válido cuando se usan referencias de punto en JS.
Hola71
2
@Daniel probablemente significaba eval ("obj." + PropName) que debería funcionar como se esperaba.
claj
41

eval(en cualquier idioma) no es malo de la misma manera que una motosierra no es mala. Es una herramienta. Resulta ser una herramienta poderosa que, cuando se usa incorrectamente, puede cortar extremidades y eviscerar (hablando metafóricamente), pero lo mismo puede decirse de muchas herramientas en la caja de herramientas de un programador, que incluyen:

  • goto y amigos
  • roscado basado en bloqueo
  • continuaciones
  • macros (higiénicas u otras)
  • punteros
  • excepciones reiniciables
  • código auto modificable
  • ... y un elenco de miles.

Si tiene que usar alguna de estas herramientas poderosas y potencialmente peligrosas, pregúntese tres veces "¿por qué?" en una cadena Por ejemplo:

"¿Por qué tengo que usar eval?" "Por culpa de foo". "¿Por qué es necesario?" "Porque ..."

Si llegas al final de esa cadena y la herramienta todavía parece que es lo correcto, entonces hazlo. Documente el infierno fuera de él. Pon a prueba el infierno. Vuelva a verificar la corrección y seguridad una y otra vez. Pero hazlo.

SOLO MI OPINIÓN correcta
fuente
Gracias, eso es lo que escuché de eval antes ("pregúntese por qué"), pero nunca había escuchado o leído cuáles son los posibles problemas. Ahora veo por las respuestas aquí cuáles son (problemas de seguridad y rendimiento).
Jay
8
Y legibilidad de código. Eval puede atornillar totalmente el flujo de código y hacerlo incomprensible.
SOLO MI OPINIÓN correcta
No entiendo por qué el "subproceso basado en bloqueo" [sic] está en su lista. Hay formas de concurrencia que no involucran bloqueos, y los problemas con los bloqueos son generalmente bien conocidos, pero nunca he oído a nadie describir el uso de bloqueos como "malvado".
asveikau
44
asveikau: El enhebrado basado en bloqueo es notoriamente difícil de corregir (supongo que el 99.44% del código de producción que usa bloqueos es malo). No se compone. Es propenso a convertir su código "multihilo" en código de serie. (Corregir esto solo hace que el código sea lento e hinchado en su lugar). Existen buenas alternativas a los subprocesos basados ​​en bloqueos, como STM o modelos de actores, que hacen que su uso en cualquier cosa que no sea el código de nivel más bajo sea malo.
SOLO MI OPINIÓN correcta
el "por qué encadenar" :) asegúrese de detenerse después de 3 pasos, puede doler.
szymanowski
27

Eval está bien, siempre y cuando sepa EXACTAMENTE lo que está pasando. Cualquier entrada del usuario que ingrese DEBE ser verificada y validada y todo. Si no sabe cómo estar 100% seguro, no lo haga.

Básicamente, un usuario puede escribir cualquier código para el idioma en cuestión, y se ejecutará. Puedes imaginar por ti mismo cuánto daño puede hacer.

Tor Valamo
fuente
1
Entonces, si realmente estoy generando expresiones S basadas en la entrada del usuario usando un algoritmo que no copiará directamente la entrada del usuario, y si eso es más fácil y claro en una situación específica que usar macros u otras técnicas, entonces supongo que no hay nada "malo" "al respecto? En otras palabras, ¿los únicos problemas con eval son los mismos con las consultas SQL y otras técnicas que utilizan la entrada del usuario directamente?
Jay
10
La razón por la que se llama "maldad" es porque hacerlo mal es mucho peor que hacer otras cosas mal. Y como sabemos, los novatos harán cosas mal.
Tor Valamo
3
No diría que el código debe ser validado antes de evaluarlo en todas las circunstancias. Al implementar un REPL simple, por ejemplo, probablemente solo alimente la entrada en eval sin marcar y eso no sería un problema (por supuesto, al escribir un REPL basado en la web necesitaría un sandbox, pero ese no es el caso para el normal CLI-REPLs que se ejecutan en el sistema del usuario).
sepp2k
1
Como dije, debe saber exactamente qué sucede cuando alimenta lo que alimenta en la evaluación. Si eso significa "ejecutará algunos comandos dentro de los límites de la caja de arena", entonces eso es lo que significa. ;)
Tor Valamo
¿@TorValamo ha oído hablar de la fuga de la cárcel?
Loïc Faure-Lacroix
21

"¿Cuándo debo usar eval?" Podría ser una mejor pregunta.

La respuesta corta es "cuando su programa está destinado a escribir otro programa en tiempo de ejecución y luego ejecutarlo". La programación genética es un ejemplo de una situación en la que probablemente tenga sentido usarla eval.

Zak
fuente
14

En mi opinión, esta pregunta no es específica de LISP . Aquí hay una respuesta sobre la misma pregunta para PHP, y se aplica a LISP, Ruby y otro lenguaje que tiene una evaluación:

Los principales problemas con eval () son:

  • Posible entrada insegura. Pasar un parámetro no confiable es una forma de fallar. A menudo no es una tarea trivial asegurarse de que un parámetro (o parte de él) sea totalmente confiable.
  • Dificultad El uso de eval () hace que el código sea inteligente, por lo tanto, más difícil de seguir. Para citar a Brian Kernighan "La depuración es dos veces más difícil que escribir el código en primer lugar. Por lo tanto, si escribe el código de la manera más inteligente posible, por definición, no es lo suficientemente inteligente como para depurarlo "

El principal problema con el uso real de eval () es solo uno:

  • desarrolladores inexpertos que lo usan sin suficiente consideración.

Tomado de aquí .

Creo que la parte difícil es un punto sorprendente. La obsesión con el código golf y el código conciso siempre ha resultado en un código "inteligente" (para el cual las evaluaciones son una gran herramienta). Pero debe escribir su código para facilitar la lectura, IMO, para no demostrar que es un astuto y no para ahorrar papel (de todos modos no lo imprimirá).

Luego, en LISP hay algún problema relacionado con el contexto en el que se ejecuta eval, por lo que el código no confiable podría tener acceso a más cosas; Este problema parece ser común de todos modos.

Dan Rosenstark
fuente
3
El problema de "entrada malvada" con EVAL solo afecta a los idiomas que no son de Lisp, porque en esos idiomas, eval () generalmente toma un argumento de cadena, y la entrada del usuario generalmente se divide. El usuario puede incluir una cita en su entrada y escapar a El código generado. Pero en Lisp, el argumento de EVAL no es una cadena, y la entrada del usuario no puede escapar al código a menos que sea absolutamente imprudente (como analizó la entrada con READ-FROM-STRING para crear una expresión S, que luego incluye en el código EVAL sin citarlo. Si lo cita, no hay forma de escapar de la cita).
Deseche la cuenta el
12

Ha habido muchas respuestas excelentes, pero aquí hay otra toma de Matthew Flatt, uno de los implementadores de Racket:

http://blog.racket-lang.org/2011/10/on-eval-in-dynamic-languages-generally.html

Explica muchos de los puntos que ya se han cubierto, pero algunas personas pueden encontrar su opinión interesante.

Resumen: el contexto en el que se utiliza afecta el resultado de la evaluación, pero los programadores a menudo no lo tienen en cuenta, lo que genera resultados inesperados.

stchang
fuente
11

La respuesta canónica es mantenerse alejado. Lo cual me parece extraño, porque es un primitivo, y de los siete primitivos (los otros son contras, auto, cdr, if, eq y quote), obtiene la menor cantidad de uso y amor.

De On Lisp : "Por lo general, llamar explícitamente a eval es como comprar algo en una tienda de regalos del aeropuerto. Habiendo esperado hasta el último momento, debe pagar precios altos por una selección limitada de productos de segunda categoría".

Entonces, ¿cuándo uso eval? Un uso normal es tener un REPL dentro de su REPL mediante evaluación (loop (print (eval (read)))). Todos están bien con ese uso.

Pero también puede definir funciones en términos de macros que se evaluarán después de la compilación combinando eval con backquote. Anda tu

(eval `(macro ,arg0 ,arg1 ,arg2))))

y matará el contexto por ti.

Swank (para emacs slime) está lleno de estos casos. Se ven así:

(defun toggle-trace-aux (fspec &rest args)
  (cond ((member fspec (eval '(trace)) :test #'equal)
         (eval `(untrace ,fspec))
         (format nil "~S is now untraced." fspec))
        (t
         (eval `(trace ,@(if args `(:encapsulate nil) (list)) ,fspec ,@args))
         (format nil "~S is now traced." fspec))))

No creo que sea un truco sucio. Lo uso todo el tiempo para reintegrar macros en funciones.

Daniel Cussen
fuente
1
Es posible que desee consultar el lenguaje del núcleo;)
artemonster
7

Otro par de puntos en Lisp eval:

  • Evalúa bajo el entorno global, perdiendo su contexto local.
  • A veces puede sentirse tentado a usar eval, cuando realmente quiso usar la macro de lectura '#'. que evalúa en el momento de la lectura.
pyb
fuente
Entiendo que el uso de env global es cierto tanto para Common Lisp como para Scheme; ¿También es cierto para Clojure?
Jay
2
En Scheme (al menos para R7RS, quizás también para R6RS) debe pasar un entorno para evaluar.
csl
4

Como la "regla" de GOTO: si no sabes lo que estás haciendo, puedes hacer un desastre.

Además de solo construir algo a partir de datos conocidos y seguros, existe el problema de que algunos lenguajes / implementaciones no pueden optimizar el código lo suficiente. Podría terminar con un código interpretado dentro eval.

stesch
fuente
¿Qué tiene que ver esa regla con GOTO? ¿Hay alguna característica en algún lenguaje de programación con la que no puedas hacer un lío?
Ken
2
@ Ken: No hay una regla GOTO, de ahí las comillas en mi respuesta. Solo hay un dogma para las personas que tienen miedo de pensar por sí mismas. Lo mismo para eval. Recuerdo haber acelerado dramáticamente un script de Perl usando eval. Es una herramienta en su caja de herramientas. Los novatos a menudo usan eval cuando otras construcciones de lenguaje son más fáciles / mejores. ¿Pero evitarlo por completo para ser genial y complacer a las personas dogmáticas?
stesch
4

Eval simplemente no es seguro. Por ejemplo, tiene el siguiente código:

eval('
hello('.$_GET['user'].');
');

Ahora el usuario llega a su sitio e ingresa la url http://example.com/file.php?user= ); $ is_admin = true; echo (

Entonces el código resultante sería:

hello();$is_admin=true;echo();
Ragnis
fuente
66
estaba hablando de Lisp pensó que no php
fmsf 03 de
44
@fmsf Estaba hablando específicamente sobre Lisp, pero generalmente sobre evalcualquier idioma que lo tenga.
Skilldrick
44
@fmsf: esta es en realidad una pregunta independiente del idioma. Incluso se aplica a lenguajes compilados estáticos, ya que pueden simular eval llamando al compilador en tiempo de ejecución.
Daniel Earwicker
1
en ese caso el idioma es un duplicado. He visto muchos como este por aquí.
fmsf
9
PHP eval no es como Lisp eval. Mire, funciona en una cadena de caracteres, y el exploit en la URL depende de poder cerrar un paréntesis textual y abrir otro. Lisp eval no es susceptible a este tipo de cosas. Puede evaluar los datos que provienen de la entrada de una red, si los protege de manera adecuada (y la estructura es lo suficientemente fácil de caminar para hacerlo).
Kaz
2

Eval no es malvada. Eval no es complicado. Es una función que compila la lista que le pasa. En la mayoría de los otros idiomas, compilar código arbitrario significaría aprender el AST del idioma y cavar en las partes internas del compilador para descubrir la API del compilador. En lisp, solo llama a eval.

¿Cuándo deberías usarlo? Siempre que necesite compilar algo, generalmente un programa que acepta, genera o modifica código arbitrario en tiempo de ejecución .

¿Cuándo no deberías usarlo? Todos los otros casos.

¿Por qué no deberías usarlo cuando no lo necesitas? Porque estaría haciendo algo de una manera innecesariamente complicada que puede causar problemas de legibilidad, rendimiento y depuración.

Sí, pero si soy un principiante, ¿cómo sé si debo usarlo? Siempre trate de implementar lo que necesita con las funciones. Si eso no funciona, agregue macros. Si eso todavía no funciona, entonces eval!

Sigue estas reglas y nunca harás mal con eval :)

optevo
fuente
0

Me gusta mucho la respuesta de Zak y ha llegado a la esencia del asunto: eval se usa cuando estás escribiendo un nuevo idioma, un guión o una modificación de un idioma. Realmente no explica más, así que daré un ejemplo:

(eval (read-line))

En este sencillo programa Lisp, se solicita al usuario que ingrese y luego se evalúa lo que ingrese. Para que esto funcione, todo el conjunto de definiciones de símbolos debe estar presente si el programa se compila, porque no tiene idea de qué funciones puede ingresar el usuario, por lo que debe incluirlas todas. Eso significa que si compila este sencillo programa, el binario resultante será gigantesco.

Por principio, ni siquiera puede considerar esto como una declaración compilable por este motivo. En general, una vez que utiliza eval , está operando en un entorno interpretado y el código ya no se puede compilar. Si no utiliza eval , puede compilar un programa Lisp o Scheme como un programa en C. Por lo tanto, desea asegurarse de que desea y necesita estar en un entorno interpretado antes de comprometerse a usar eval .

Tyler Durden
fuente