web-dev-qa-db-fra.com

Pourquoi eval est-il exactement mauvais?

Je sais que les programmeurs LISP et Scheme disent généralement que eval doit être évité, sauf si cela est strictement nécessaire. J'ai vu la même recommandation pour plusieurs langages de programmation, mais je n'ai pas encore vu de liste d'arguments clairs contre l'utilisation de eval. Où puis-je trouver un compte rendu des problèmes potentiels liés à l'utilisation de eval?

Par exemple, je connais les problèmes de GOTO dans la programmation procédurale (rend les programmes illisibles et difficiles à maintenir, rend les problèmes de sécurité difficiles à trouver, etc.), mais je n'ai jamais vu les arguments contre eval.

Fait intéressant, les mêmes arguments contre GOTO devraient être valables contre les continuations, mais je vois que Schemers, par exemple, ne dira pas que les continuations sont "mauvaises" - vous devez juste être prudent lorsque vous les utilisez. Ils sont beaucoup plus susceptibles de désapprouver le code en utilisant eval que le code en utilisant des continuations (pour autant que je puisse voir - je peux me tromper).

140
Jay

Il y a plusieurs raisons pour lesquelles il ne faut pas utiliser EVAL.

La raison principale pour les débutants est: vous n'en avez pas besoin.

Exemple (en supposant LISP commun):

ÉVALUER une expression avec différents opérateurs:

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

C'est mieux écrit comme:

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

Il existe de nombreux exemples où les débutants apprenant LISP pensent avoir besoin de EVAL, mais ils n'en ont pas besoin - car les expressions sont évaluées et on peut également évaluer la partie fonction. La plupart du temps, l'utilisation de EVAL montre un manque de compréhension de l'évaluateur.

C'est le même problème avec les macros. Souvent, les débutants écrivent des macros, où ils doivent écrire des fonctions - ne pas comprendre à quoi servent vraiment les macros et ne pas comprendre qu'une fonction fait déjà le travail.

C'est souvent le mauvais outil pour le travail d'utiliser EVAL et cela indique souvent que le débutant ne comprend pas les règles d'évaluation LISP habituelles.

Si vous pensez avoir besoin de EVAL, vérifiez si quelque chose comme FUNCALL, REDUCE ou APPLY pourrait être utilisé à la place.

  • FUNCALL - appeler une fonction avec des arguments: (funcall '+ 1 2 3)
  • REDUCE - appeler une fonction sur une liste de valeurs et combiner les résultats: (reduce '+ '(1 2 3))
  • APPLY - appeler une fonction avec une liste comme arguments: (apply '+ '(1 2 3)).

Q: ai-je vraiment besoin d'eval ou le compilateur/évaluateur a-t-il déjà ce que je veux vraiment?

Les principales raisons d'éviter EVAL pour les utilisateurs légèrement plus avancés:

  • vous voulez vous assurer que votre code est compilé, car le compilateur peut vérifier le code pour de nombreux problèmes et génère un code plus rapide, parfois BEAUCOUP BEAUCOUP (c'est le facteur 1000 ;-)) un code plus rapide

  • le code qui est construit et doit être évalué ne peut pas être compilé le plus tôt possible.

  • l'évaluation d'une entrée arbitraire de l'utilisateur ouvre des problèmes de sécurité

  • une certaine utilisation de l'évaluation avec EVAL peut se produire au mauvais moment et créer des problèmes de construction

Pour expliquer le dernier point avec un exemple simplifié:

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

Donc, je peux vouloir écrire une macro qui basée sur le premier paramètre utilise SIN ou COS.

(foo 3 4) Est-ce que (sin 4) et (foo 1 4) Est-ce que (cos 4).

Maintenant, nous pouvons avoir:

(foo (+ 2 1) 4)

Cela ne donne pas le résultat souhaité.

On peut alors vouloir réparer la macro FOO en ÉVALUANT la variable:

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

(foo (+ 2 1) 4)

Mais cela ne fonctionne toujours pas:

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

La valeur de la variable n'est tout simplement pas connue au moment de la compilation.

ne raison générale importante pour éviter EVAL: il est souvent utilisé pour les hacks laids.

147
Rainer Joswig

eval (dans n'importe quelle langue) n'est pas mal de la même manière qu'une tronçonneuse n'est pas mal. C'est un outil. Il se trouve que c'est un outil puissant qui, lorsqu'il est mal utilisé, peut couper des membres et éviscérer (métaphoriquement), mais la même chose peut être dite pour de nombreux outils dans la boîte à outils d'un programmeur, notamment:

  • goto et amis
  • filetage basé sur un verrou
  • suites
  • macros (hygiéniques ou autres)
  • pointeurs
  • exceptions redémarrables
  • code auto-modifiable
  • ... et un casting de milliers.

Si vous devez utiliser l'un de ces outils puissants et potentiellement dangereux, demandez-vous trois fois "pourquoi?" dans une chaîne. Par exemple:

"Pourquoi dois-je utiliser eval?" "A cause de foo." "Pourquoi foo est-il nécessaire?" "Car ..."

Si vous arrivez au bout de cette chaîne et que l'outil semble toujours être la bonne chose à faire, faites-le. Documentez l'enfer hors de lui. Testez l'enfer hors de lui. Vérifiez encore et encore l'exactitude et la sécurité. Mais fais-le.

L'évaluation est très bien, tant que vous savez EXACTEMENT ce qui s'y passe. Toute entrée d'utilisateur y entrant DOIT être vérifiée et validée et tout. Si vous ne savez pas comment être sûr à 100%, ne le faites pas.

Fondamentalement, un utilisateur peut taper n'importe quel code pour la langue en question et il s'exécutera. Vous pouvez vous imaginer combien de dégâts il peut faire.

26
Tor Valamo

"Quand dois-je utiliser eval?" pourrait être une meilleure question.

La réponse courte est "lorsque votre programme est destiné à écrire un autre programme au moment de l'exécution, puis à l'exécuter". Programmation génétique est un exemple de situation où il est probablement judicieux d'utiliser eval.

21
Zak

OMI, cette question n'est pas spécifique au LISP . Voici une réponse à la même question pour PHP, et elle s'applique à LISP, Ruby et tout autre langage qui a un eval:

Les principaux problèmes avec eval () sont:

  • Entrée potentiellement dangereuse. Passer un paramètre non approuvé est un moyen d'échouer. Il n'est souvent pas facile de s'assurer qu'un paramètre (ou une partie de celui-ci) est entièrement fiable.
  • Trickyness. L'utilisation de eval () rend le code intelligent, donc plus difficile à suivre. Pour citer Brian Kernighan " Le débogage est deux fois plus difficile que d'écrire le code en premier lieu. Par conséquent, si vous écrivez le code aussi intelligemment que possible, vous n'êtes, par définition, pas assez intelligent pour le déboguer "

Le principal problème avec l'utilisation réelle de eval () n'en est qu'un:

  • développeurs inexpérimentés qui l'utilisent sans suffisamment de considération.

Tiré de ici .

Je pense que la pièce délicate est un point étonnant. L'obsession du code golf et du code concis a toujours débouché sur un code "intelligent" (pour lequel les Evals sont un excellent outil). Mais vous devez écrire votre code pour plus de lisibilité, OMI, pour ne pas démontrer que vous êtes un smarty et pour ne pas économiser du papier (vous n'imprimerez pas de toute façon).

Ensuite, dans LISP, il y a un problème lié au contexte dans lequel eval est exécuté, donc le code non approuvé pourrait avoir accès à plus de choses; ce problème semble être courant de toute façon.

14
Dan Rosenstark

Il y a eu beaucoup de bonnes réponses, mais voici une autre prise de Matthew Flatt, l'un des implémenteurs de Racket:

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

Il soulève bon nombre des points qui ont déjà été abordés, mais certaines personnes peuvent néanmoins trouver son point de vue intéressant.

Résumé: Le contexte dans lequel il est utilisé affecte le résultat de l'évaluation, mais n'est souvent pas pris en compte par les programmeurs, ce qui conduit à des résultats inattendus.

12
stchang

La réponse canonique est de rester à l'écart. Ce que je trouve bizarre, car c'est une primitive, et parmi les sept primitives (les autres étant contre, car, cdr, if, eq et quote), elle obtient de loin le moins d'utilisation et d'amour.

De Sur LISP: "Habituellement, appeler explicitement eval, c'est comme acheter quelque chose dans une boutique de cadeaux d'aéroport. Après avoir attendu jusqu'au dernier moment, vous devez payer des prix élevés pour une sélection limitée de produits de second ordre. . "

Alors, quand dois-je utiliser eval? Une utilisation normale consiste à avoir un REPL dans votre REPL en évaluant (loop (print (eval (read)))). Tout le monde est d'accord avec cette utilisation.

Mais vous pouvez également définir des fonctions en termes de macros qui seront évaluées après compilation en combinant eval avec backquote. Tu vas

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

et cela tuera le contexte pour vous.

Swank (pour emacs slime) est plein de ces cas. Ils ressemblent à ceci:

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

Je ne pense pas que ce soit un hack sale. Je l'utilise tout le temps moi-même pour réintégrer des macros dans des fonctions.

11
Daniel Cussen

Encore quelques points sur LISP eval:

  • Il évalue dans l'environnement mondial, perdant votre contexte local.
  • Parfois, vous pouvez être tenté d'utiliser eval, alors que vous vouliez vraiment utiliser la macro de lecture '#.' qui évalue au moment de la lecture.
7
pyb

Comme la "règle" GOTO: si vous ne savez pas ce que vous faites, vous pouvez faire un gâchis.

En plus de ne construire que quelque chose à partir de données connues et sûres, il y a le problème que certaines langues/implémentations ne peuvent pas suffisamment optimiser le code. Vous pourriez vous retrouver avec du code interprété dans eval.

4
stesch

Eval n'est pas sûr. Par exemple, vous avez le code suivant:

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

L'utilisateur vient maintenant sur votre site et saisit l'url http://example.com/file.php?user= ); $ is_admin = true; echo (

Le code résultant serait alors:

hello();$is_admin=true;echo();
4
Ragnis

Eval n'est pas mal. L'évaluation n'est pas compliquée. C'est une fonction qui compile la liste que vous lui passez. Dans la plupart des autres langages, compiler du code arbitraire signifierait apprendre le langage AST et fouiller dans les internes du compilateur pour comprendre l'API du compilateur. Dans LISP, vous appelez simplement eval.

Quand devriez-vous l'utiliser? Chaque fois que vous devez compiler quelque chose, généralement un programme qui accepte, génère ou modifie code arbitraire à l'exécution.

Quand ne devriez-vous pas l'utiliser? Tous les autres cas.

Pourquoi ne devriez-vous pas l'utiliser lorsque vous n'en avez pas besoin? Parce que vous feriez quelque chose d'une manière inutilement compliquée qui pourrait causer des problèmes de lisibilité, de performances et de débogage.

Oui, mais si je suis débutant, comment savoir si je dois l'utiliser? Essayez toujours d'implémenter ce dont vous avez besoin avec des fonctions. Si cela ne fonctionne pas, ajoutez des macros. Si cela ne fonctionne toujours pas, alors évaluez!

Suivez ces règles et vous ne ferez jamais de mal avec eval :)

2
optevo

J'aime beaucoup la réponse de Zak et il est arrivé à l'essentiel: eval est utilisé lorsque vous écrivez un nouvelle langue, script ou modification d'une langue. Il n'explique pas vraiment plus, je vais donc donner un exemple:

(eval (read-line))

Dans ce programme LISP simple, l'utilisateur est invité à entrer, puis tout ce qu'il saisit est évalué. Pour que cela fonctionne, l'ensemble complet des définitions de symboles doit être présent si le programme est compilé, car vous n'avez aucune idée des fonctions que l'utilisateur peut entrer, vous devez donc tous les inclure. Cela signifie que si vous compilez ce programme simple, le binaire résultant sera gigantesque.

Par principe, vous ne pouvez même pas considérer cela comme une déclaration compilable pour cette raison. En général, une fois que vous utilisez eval , vous travaillez dans un environnement interprété et le code ne peut plus être compilé. Si vous n'utilisez pas eval , vous pouvez compiler un programme LISP ou Scheme comme un programme C. Par conséquent, vous devez vous assurer que vous voulez et devez être dans un environnement interprété avant de vous engager à utiliser eval .

0
Tyler Durden