Pourquoi exactement eval est-il mauvais?

141

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

Par exemple, je connais les problèmes de la GOTOprogrammation 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 GOTOdevraient ê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 froncer les sourcils sur l'utilisation de code evalque sur le code utilisant des continuations (pour autant que je sache, je peux me tromper).

Geai
la source
5
eval n'est pas le mal, mais le mal est ce que fait eval
Anurag
9
@yar - Je pense que votre commentaire indique une vision du monde très centrée sur les objets d'expédition. C'est probablement valable pour la plupart des langages, mais ce serait différent dans Common Lisp, où les méthodes n'appartiennent pas aux classes et encore plus différent dans Clojure, où les classes ne sont supportées que par les fonctions d'interopérabilité Java. Jay a étiqueté cette question comme Scheme, qui n'a aucune notion intégrée de classes ou de méthodes (diverses formes d'OO sont disponibles sous forme de bibliothèques).
Zak
3
@Zak, vous avez raison, je ne connais que les langues que je connais, mais même si vous travaillez avec un document Word sans utiliser les styles, vous n'êtes pas sec. Mon propos était d'utiliser la technologie pour ne pas vous répéter. OO n'est pas universel, vrai ...
Dan Rosenstark
4
J'ai pris la liberté d'ajouter la balise clojure à cette question, car je pense que les utilisateurs de Clojure pourraient bénéficier d'une exposition aux excellentes réponses publiées ici.
Michał Marczyk
... eh bien, pour Clojure, au moins une raison supplémentaire s'applique: vous perdez la compatibilité avec ClojureScript et ses dérivés.
Charles Duffy

Réponses:

148

Il y a plusieurs raisons pour lesquelles on ne devrait pas utiliser EVAL.

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

Exemple (en supposant Common Lisp):

É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 suit:

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

Il y a beaucoup d'exemples où les débutants apprenant Lisp pensent qu'ils ont besoin EVAL, mais ils n'en ont pas besoin - puisque les expressions sont évaluées et on peut également évaluer la partie fonction. La plupart du temps, l'utilisation de EVALmontre 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 devraient écrire des fonctions - ne comprenant pas à quoi servent réellement les macros et ne comprenant pas qu'une fonction fait déjà le travail.

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

Si vous pensez que vous en avez besoin EVAL, vérifiez si quelque chose comme FUNCALL, REDUCEou APPLYpourrait ê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 que les arguments: (apply '+ '(1 2 3)).

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

Les principales raisons à éviter EVALpour 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 du code plus rapide, parfois BEAUCOUP BEAUCOUP (c'est le facteur 1000 ;-)) code plus rapide

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

  • L'évaluation des entrées arbitraires de l'utilisateur pose des problèmes de sécurité

  • une certaine utilisation de l'évaluation avec EVALpeut 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 soit SINou COS.

(foo 3 4)fait (sin 4)et (foo 1 4)fait (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 FOOen évaluant la variable:

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

(foo (+ 2 1) 4)

Mais alors 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.

Une raison générale importante à éviter EVAL: il est souvent utilisé pour des hacks laids.

Rainer Joswig
la source
3
Merci! Je n'ai simplement pas compris le dernier point (évaluation au mauvais moment?) - Pouvez-vous élaborer un peu s'il vous plaît?
Jay
41
+1 car c'est la vraie réponse - les gens se rabattent evalsimplement parce qu'ils ne savent pas qu'il existe une langue ou une fonctionnalité de bibliothèque spécifique pour faire ce qu'ils veulent faire. Exemple similaire de JS: je veux obtenir une propriété d'un objet en utilisant un nom dynamique, alors j'écris: eval("obj.+" + propName)quand j'aurais pu écrire obj[propName].
Daniel Earwicker
Je vois ce que tu veux dire maintenant, Rainer! Merci!
Jay
@ Daniel: "obj.+"? La dernière fois que j'ai vérifié, +n'est pas valide lors de l'utilisation de références de points dans JS.
Hello71
2
@Daniel signifiait probablement eval ("obj." + PropName) qui devrait fonctionner comme prévu.
claj
41

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 parlant), mais il en va de même pour de nombreux outils dans la boîte à outils d'un programmeur, notamment:

  • goto et amis
  • thread basé sur le verrouillage
  • continuations
  • 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 l'exactitude et la sécurité encore et encore et encore. Mais fais-le.

JUSTE MON AVIS correct
la source
Merci - c'est ce que j'ai entendu parler d'eval avant ("demandez-vous pourquoi"), mais je n'avais encore jamais entendu ou lu quels sont les problèmes potentiels. Je vois maintenant à partir des réponses ici ce qu'ils sont (problèmes de sécurité et de performances).
Jay
8
Et la lisibilité du code. Eval peut totalement visser le flux de code et le rendre incompréhensible.
JUSTE MON OPINION correcte
Je ne comprends pas pourquoi le "threading basé sur le verrouillage" [sic] est dans votre liste. Il existe des formes de concurrence d'accès qui n'impliquent pas de verrous, et les problèmes avec les verrous sont généralement bien connus, mais je n'ai jamais entendu personne décrire l'utilisation des verrous comme "diabolique".
asveikau
4
asveikau: Le threading basé sur les verrous est notoirement difficile à obtenir (je suppose que 99,44% du code de production utilisant des verrous est mauvais). Ça ne compose pas. Il est susceptible de transformer votre code "multi-thread" en code de série. (Corriger cela rend simplement le code lent et gonflé à la place.) Il existe de bonnes alternatives au threading basé sur les verrous, comme les modèles STM ou d'acteurs, qui en font un mauvais usage sauf dans le code de plus bas niveau.
JUSTE MON OPINION correcte
la chaîne "pourquoi" :) assurez-vous d'arrêter après 3 étapes, cela peut faire mal.
szymanowski
27

Eval est très bien, tant que vous savez exactement ce qui se passe. Toute entrée d'utilisateur qui y est entrée 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 imaginer les dégâts qu'il peut faire.

Tor Valamo
la source
1
Donc, si je génère réellement des expressions S basées sur l'entrée utilisateur à l'aide d'un algorithme qui ne copie pas directement l'entrée utilisateur, et si c'est plus facile et plus clair dans une situation spécifique que d'utiliser des macros ou d'autres techniques, alors je suppose qu'il n'y a rien de mal " à propos de ça? En d'autres termes, les seuls problèmes avec eval sont les mêmes avec les requêtes SQL et d'autres techniques qui utilisent directement l'entrée utilisateur?
Jay
10
La raison pour laquelle on l'appelle «mal» est que le mal est bien pire que de mal faire d'autres choses. Et comme nous le savons, les débutants feront des choses mal.
Tor Valamo
3
Je ne dirais pas que le code doit être validé avant de l'évaluer en toutes circonstances. Lors de la mise en œuvre d'un REPL simple par exemple, vous ne feriez probablement que nourrir l'entrée dans eval non cochée et ce ne serait pas un problème (bien sûr, lors de l'écriture d'une REPL basée sur le Web, vous auriez besoin d'un bac à sable, mais ce n'est pas le cas pour la normale CLI-REPL qui s'exécutent sur le système de l'utilisateur).
sepp2k
1
Comme je l'ai dit, vous devez savoir exactement ce qui se passe lorsque vous introduisez ce que vous alimentez dans l'évaluation. Si cela signifie "il exécutera certaines commandes dans les limites du bac à sable", alors c'est ce que cela signifie. ;)
Tor Valamo
@TorValamo a déjà entendu parler d'une évasion de prison?
Loïc Faure-Lacroix
21

"Quand devrais-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". La programmation génétique est un exemple de situation dans laquelle son utilisation est probablement logique eval.

Zak
la source
14

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 à d'autres langages qui ont une évaluation:

Les principaux problèmes avec eval () sont:

  • Entrée potentiellement dangereuse. Passer un paramètre non approuvé est un moyen d'échouer. Ce n'est souvent pas une tâche facile de s'assurer qu'un paramètre (ou une partie de celui-ci) est entièrement fiable.
  • Trickyness. Utiliser eval () rend le code intelligent, donc plus difficile à suivre. Pour citer Brian Kernighan "Le débogage est deux fois plus difficile que l'écriture du 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 () est un seul:

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

Pris d' ici .

Je pense que la pièce délicate est un point incroyable. L'obsession du code golf et du code concis a toujours abouti à un code "intelligent" (pour lequel les evals sont un excellent outil). Mais vous devriez écrire votre code pour plus de lisibilité, IMO, pour ne pas démontrer que vous êtes intelligent et pour ne pas économiser du papier (vous ne l'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 du code non approuvé pourrait avoir accès à plus de choses; ce problème semble de toute façon courant.

Dan Rosenstark
la source
3
Le problème de "mauvaise entrée" avec EVAL n'affecte que les langages non-Lisp, car dans ces langages, eval () prend généralement un argument de type chaîne, et l'entrée de l'utilisateur est généralement ajoutée. L'utilisateur peut inclure un guillemet dans son entrée et s'échapper dans le code généré. Mais en Lisp, l'argument d'EVAL n'est pas une chaîne, et l'entrée utilisateur ne peut pas s'échapper dans le code sauf si vous êtes absolument imprudent (comme dans vous avez analysé l'entrée avec READ-FROM-STRING pour créer une expression S, que vous incluez ensuite dans le code EVAL sans le citer. Si vous le citez, il n'y a aucun moyen d'échapper à la citation).
Compte
12

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 fait valoir 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 eval mais n'est souvent pas pris en compte par les programmeurs, ce qui entraîne des résultats inattendus.

stchang
la source
11

La réponse canonique est de rester à l'écart. Ce que je trouve bizarre, car c'est un primitif, et des sept primitifs (les autres étant contre, voiture, cdr, if, eq et quote), il obtient de loin le moins d'utilisation et d'amour.

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

Alors, quand utiliser eval? Une utilisation normale est d'avoir une 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 sale hack. Je l'utilise moi-même tout le temps pour réintégrer des macros dans des fonctions.

Daniel Cussen
la source
1
Vous voudrez peut-être vérifier le langage du noyau;)
artemonster
7

Un autre couple de points sur l'évaluation Lisp:

  • Il évalue dans l'environnement global, perdant votre contexte local.
  • Parfois, vous pouvez être tenté d'utiliser eval, alors que vous avez vraiment l'intention d'utiliser la macro de lecture «#». qui évalue au moment de la lecture.
pyb
la source
Je comprends que l'utilisation d'environnement global est vraie pour Common Lisp et Scheme; est-ce également vrai pour Clojure?
Jay
2
Dans Scheme (au moins pour R7RS, peut-être aussi pour R6RS), vous devez passer un environnement à eval.
csl
4

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 certains langages / implémentations ne peuvent pas optimiser suffisamment le code. Vous pourriez vous retrouver avec du code interprété à l'intérieur eval.

stesch
la source
Qu'est-ce que cette règle a à voir avec GOTO? Y a-t-il une fonctionnalité dans un langage de programmation avec laquelle vous ne pouvez pas faire de désordre?
Ken le
2
@Ken: Il n'y a pas de règle GOTO, d'où les guillemets dans ma réponse. Il y a juste un dogme pour les gens qui ont peur de penser par eux-mêmes. Idem pour eval. Je me souviens avoir accéléré considérablement certains scripts Perl en utilisant eval. C'est un outil dans votre boîte à outils. Les débutants utilisent souvent eval lorsque d'autres constructions de langage sont plus faciles / meilleures. Mais l'éviter complètement juste pour être cool et faire plaisir aux dogmatiques?
stesch
4

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

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

Maintenant, l'utilisateur accède à votre site et entre l'URL http://example.com/file.php?user= ); $ is_admin = true; echo (

Ensuite, le code résultant serait:

hello();$is_admin=true;echo();
Ragnis
la source
6
il parlait de Lisp pensait pas php
fmsf
4
@fmsf Il parlait spécifiquement de Lisp, mais généralement de evaln'importe quel langage qui en avait.
Skilldrick
4
@fmsf - c'est en fait une question indépendante de la langue. Cela s'applique même aux langages compilés statiques car ils peuvent simuler eval en appelant le compilateur au moment de l'exécution.
Daniel Earwicker
1
dans ce cas, la langue est un doublon. J'en ai vu beaucoup comme celui-ci ici.
fmsf
9
PHP eval n'est pas comme Lisp eval. Regardez, cela fonctionne sur une chaîne de caractères, et l'exploit dans l'URL dépend de la possibilité de fermer une parenthèse textuelle et d'en ouvrir une autre. Lisp eval n'est pas sensible à ce genre de choses. Vous pouvez évaluer les données qui proviennent d'un réseau en entrée, si vous les sandboxez correctement (et la structure est assez facile à parcourir pour ce faire).
Kaz
2

Eval n'est pas mauvais. 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 l'AST du langage et fouiller dans les composants internes du compilateur pour comprendre l'API du compilateur. Dans lisp, vous appelez simplement eval.

Quand devriez-vous l'utiliser? Chaque fois que vous avez besoin de compiler quelque chose, généralement un programme qui accepte, génère ou modifie du code arbitraire au moment de 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.

Ouais, mais si je suis un 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, évaluez!

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

optevo
la source
0

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

(eval (read-line))

Dans ce programme Lisp simple, l'utilisateur est invité à entrer, puis tout ce qu'il entre est évalué. Pour que cela fonctionne l' ensemble ensemble de définitions de symboles doit être présent si le programme est compilé, parce que vous ne savez pas quelles fonctions l'utilisateur peut entrer, vous devez les inclure. Cela signifie que si vous compilez ce programme simple, le binaire résultant sera gigantesque.

En 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 voulez vous assurer que vous voulez et devez être dans un environnement interprété avant de vous engager à utiliser eval .

Tyler Durden
la source