clojure - operator - ¿Por qué exactamente eval evalúa mal?
reduce in clojure (12)
"¿Cuándo debería usar eval
?" podría ser una mejor pregunta.
La respuesta breve 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 usar eval
.
Sé que los programadores de Lisp y Scheme generalmente dicen que se debe evitar la eval
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 contra el uso de eval
. ¿Dónde puedo encontrar una cuenta de los problemas potenciales del uso de eval
?
Por ejemplo, conozco los problemas de GOTO
en la programación de procedimientos (hace que los programas sean ilegibles y difíciles de mantener, hace que los problemas de seguridad sean difíciles de encontrar, etc.), pero nunca he visto los argumentos en contra de la eval
.
Curiosamente, los mismos argumentos en contra de GOTO
deberían ser válidos contra las continuaciones, pero veo que Schemers, por ejemplo, no dirá que las continuidades son "malas"; solo debe tener cuidado al usarlas. Son mucho más propensos a fruncir el ceño sobre el código usando eval
que sobre el código usando continuaciones (por lo que puedo ver, podría estar equivocado).
Como la "regla" de GOTO: si no sabes lo que estás haciendo, puedes hacer un desastre.
Además de solo crear 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 de eval
.
Eval es simplemente inseguro. Por ejemplo, tiene el siguiente código:
eval(''
hello(''.$_GET[''user''].'');
'');
Ahora el usuario accede a su sitio y ingresa url http://example.com/file.php?user= ); $ is_admin = true; echo (
Entonces el código resultante sería:
hello();$is_admin=true;echo();
Eval está bien, siempre y cuando se sepa EXACTAMENTE qué es lo que entra. Cualquier entrada de usuario que ingrese DEBE ser verificada y validada, y todo. Si no sabes cómo estar 100% seguro, entonces no lo hagas.
Básicamente, un usuario puede escribir cualquier código para el idioma en cuestión y se ejecutará. Puedes imaginarte cuánto daño puede hacer.
Eval no es malvado Eval no es complicado. Es una función que compila la lista que le pasa. En la mayoría de los otros lenguajes, la compilación de código arbitrario significaría aprender el AST del idioma y explorar en el interior del compilador para descubrir la API del compilador. En lisp, simplemente 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 demás casos.
¿Por qué no deberías usarlo cuando no es necesario? Porque estarías haciendo algo innecesariamente complicado que puede causar problemas de legibilidad, rendimiento y depuración.
Sí, pero si soy un principiante, ¿cómo sé si debería 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 :)
Ha habido muchas respuestas excelentes, pero esta es 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
Él hace muchos de los puntos que ya se han cubierto, pero algunas personas pueden encontrar su toma interesante, no obstante.
Resumen: el contexto en el que se utiliza afecta el resultado de la evaluación, pero a menudo no es considerado por los programadores, lo que lleva a resultados inesperados.
Hay varias razones por las cuales uno no debería usar EVAL
.
La razón principal para los principiantes es: no lo necesitas.
Ejemplo (suponiendo Common Lisp):
EVALua 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 donde los principiantes que están aprendiendo Lisp creen 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 EVAL
muestra 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 comprender que una función ya hace el trabajo.
A menudo es la herramienta incorrecta para el trabajo usar EVAL
y a menudo indica que el principiante no comprende las reglas de evaluación de Lisp habituales.
Si crees que necesitas EVAL
, entonces verifica si se puede usar algo como FUNCALL
, REDUCE
o APPLY
.
-
FUNCALL
- llama a una función con argumentos:(funcall ''+ 1 2 3)
-
REDUCE
: llama a una función en una lista de valores y combina los resultados:(reduce ''+ ''(1 2 3))
-
APPLY
- llama a una función con una lista como argumentos:(apply ''+ ''(1 2 3))
.
P: ¿Realmente necesito evaluar o el compilador / evaluador ya es lo que realmente quiero?
Las principales razones para evitar EVAL
para usuarios un poco más avanzados:
usted quiere asegurarse de que su código esté compilado, porque el compilador puede verificar el código para muchos problemas y genera un código más rápido, a veces MUCHO MUCHO (ese es el factor 1000 ;-)) código más rápido
el código que se construye y necesita ser evaluado no se puede compilar tan pronto como sea posible.
La evaluación de la entrada de usuario arbitraria abre los problemas de seguridad
algunos usos de la evaluación con
EVAL
pueden ocurrir en el momento equivocado y crear problemas de construcció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 SIN
o COS
.
(foo 3 4)
does (sin 4)
y (foo 1 4)
does (cos 4)
.
Ahora podemos tener:
(foo (+ 2 1) 4)
Esto no da el resultado deseado.
Uno puede querer reparar el macro FOO
al EVALUAR la variable:
(defmacro foo (a b)
(list (if (eql (eval a) 3) ''sin ''cos) b))
(foo (+ 2 1) 4)
Pero luego, esto aún no funciona:
(defun bar (a b)
(foo a b))
El valor de la variable simplemente no se conoce en el momento de la compilación.
Una razón general importante para evitar EVAL
: a menudo se usa para hacks feos.
La respuesta canónica es mantenerse alejado. Lo cual me parece raro, porque es un primitivo, y de los siete primitivos (los otros son cons, coche, cdr, if, eq y quote), obtiene de lejos la menor cantidad de uso y amor.
De On Lisp : "Generalmente, llamar a eval explícitamente es como comprar algo en una tienda de regalos del aeropuerto. Habiendo esperado hasta el último momento, tienes que 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 la 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 para ti.
Swank (para baba de emacs) 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 hack sucio. Lo uso todo el tiempo para reinsertar macros en funciones.
Me gusta mucho la respuesta de Zak y él ha captado la esencia del asunto: eval se usa cuando estás escribiendo un nuevo idioma, un guión o modificación de un idioma. Él realmente no explica más, así que daré un ejemplo:
(eval (read-line))
En este sencillo programa Lisp, al usuario se le solicita una entrada y luego se evalúa todo lo que ingresa. Para que esto funcione, el conjunto completo de definiciones de símbolos debe estar presente si el programa está compilado, 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.
Como cuestión de principio, ni siquiera puede considerar esto como una declaración compilable por este motivo. En general, una vez que usa eval , está operando en un entorno interpretado y el código ya no se puede compilar. Si no utiliza eval, entonces puede compilar un programa Lisp o Scheme como un programa C. Por lo tanto, desea asegurarse de que desea y necesita estar en un entorno interpretado antes de comprometerse a usar eval .
OMI, 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 un eval:
Los principales problemas con eval () son:
- Potencial entrada insegura. Pasar un parámetro que no es de confianza es una forma de error. A menudo no es una tarea trivial asegurarse de que un parámetro (o parte de él) sea totalmente confiable.
- Trickyness. 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 problema principal con el uso real de eval () es solo uno:
- desarrolladores sin experiencia que lo usan sin suficiente consideración.
Tomado de here .
Creo que la pieza de engaño es un punto increíble. La obsesión con el código de 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 de legibilidad, IMO, para no demostrar que es inteligente y no para guardar papel (de todos modos no lo imprimirá).
Luego, en LISP hay un problema relacionado con el contexto en el que eval se ejecuta, por lo que el código que no es de confianza podría tener acceso a más cosas; este problema parece ser común de todos modos.
Otro par de puntos en Lisp eval:
- Evalúa bajo el entorno global, perdiendo su contexto local.
- A veces puede estar tentado de usar eval, cuando realmente quería usar el read-macro ''#''. que se evalúa en tiempo de lectura.
eval
(en cualquier idioma) no es malo de la misma manera que una motosierra no es malvada. Es una herramienta. Resulta ser una herramienta poderosa que, cuando se usa mal, puede cortar extremidades y eviscerar (metafóricamente hablando), pero lo mismo puede decirse de muchas herramientas en la caja de herramientas de un programador, que incluyen:
-
goto
y amigos - Enhebrado basado en bloqueo
- continuaciones
- macros (hygenic u otros)
- punteros
- excepciones reiniciables
- código de auto modificación
- ... y un elenco de miles.
Si tiene que usar cualquiera 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 foo". "¿Por qué es tan necesario?" "Porque ..."
Si llega al final de esa cadena y la herramienta parece que es lo correcto, hágalo. Documenta el infierno fuera de eso. Prueba el infierno fuera de eso. Verifique dos veces la corrección y la seguridad una y otra vez. Pero hazlo.