Warum genau ist eval böse?

141

Ich weiß, dass Lisp- und Scheme-Programmierer normalerweise sagen, dass evaldies vermieden werden sollte, es sei denn, dies ist unbedingt erforderlich. Ich habe die gleiche Empfehlung für mehrere Programmiersprachen gesehen, aber noch keine Liste klarer Argumente gegen die Verwendung von eval. Wo finde ich einen Bericht über die möglichen Probleme bei der Verwendung eval?

Zum Beispiel kenne ich die Probleme bei der GOTOprozeduralen Programmierung (macht Programme unlesbar und schwer zu warten, macht Sicherheitsprobleme schwer zu finden usw.), aber ich habe nie die Argumente dagegen gesehen eval.

Interessanterweise sollten dieselben Argumente gegen GOTOFortsetzungen gültig sein, aber ich sehe, dass Schemers zum Beispiel nicht sagt, dass Fortsetzungen "böse" sind - Sie sollten nur vorsichtig sein, wenn Sie sie verwenden. Es ist viel wahrscheinlicher, dass sie bei der Verwendung evalvon Code die Stirn runzeln als bei der Verwendung von Fortsetzungen (soweit ich sehen kann - ich könnte mich irren).

Jay
quelle
5
eval ist nicht böse, aber böse ist das, was eval tut
Anurag
9
@yar - Ich denke, Ihr Kommentar weist auf eine sehr objektorientierte Weltanschauung mit einem einzigen Versand hin. Es ist wahrscheinlich für die meisten Sprachen gültig, würde sich jedoch in Common Lisp unterscheiden, wo Methoden nicht zu Klassen gehören, und noch mehr in Clojure, wo Klassen nur über Java-Interop-Funktionen unterstützt werden. Jay hat diese Frage als Schema markiert, das keine eingebaute Vorstellung von Klassen oder Methoden hat (verschiedene Formen von OO sind als Bibliotheken verfügbar).
Zak
3
@Zak, Sie haben Recht, ich kenne nur die Sprachen, die ich kenne, aber selbst wenn Sie mit einem Word-Dokument arbeiten, ohne Stile zu verwenden, sind Sie nicht trocken. Mein Ziel war es, die Technologie zu nutzen, um sich nicht zu wiederholen. OO ist nicht universell, wahr ...
Dan Rosenstark
4
Ich habe mir erlaubt, dieser Frage das Clojure-Tag hinzuzufügen, da ich glaube, dass Clojure-Benutzer von den hervorragenden Antworten profitieren können, die hier veröffentlicht werden.
Michał Marczyk
... für Clojure gilt mindestens ein weiterer Grund: Sie verlieren die Kompatibilität mit ClojureScript und seinen Derivaten.
Charles Duffy

Antworten:

148

Es gibt mehrere Gründe, warum man nicht verwenden sollte EVAL.

Der Hauptgrund für Anfänger ist: Sie brauchen es nicht.

Beispiel (unter der Annahme von Common Lisp):

BEWERTEN Sie einen Ausdruck mit verschiedenen Operatoren:

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

Das ist besser geschrieben als:

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

Es gibt viele Beispiele, bei denen Anfänger, die Lisp lernen, glauben EVAL, dass sie es brauchen , aber sie brauchen es nicht - da Ausdrücke ausgewertet werden und man auch den Funktionsteil bewerten kann. Meistens EVALzeigt die Verwendung von ein Unverständnis des Bewerters.

Es ist das gleiche Problem mit Makros. Oft schreiben Anfänger Makros, in die sie Funktionen schreiben sollen - ohne zu verstehen, wofür Makros wirklich sind, und ohne zu verstehen, dass eine Funktion bereits die Aufgabe erfüllt.

Es ist oft das falsche Werkzeug für den Job EVALund zeigt oft an, dass der Anfänger die üblichen Lisp-Bewertungsregeln nicht versteht.

Wenn Sie denken , Sie brauchen EVAL, dann prüfen , ob so etwas wie FUNCALL, REDUCEoder APPLYkönnte stattdessen verwendet werden.

  • FUNCALL - eine Funktion mit Argumenten aufrufen: (funcall '+ 1 2 3)
  • REDUCE - Rufen Sie eine Funktion in einer Werteliste auf und kombinieren Sie die Ergebnisse: (reduce '+ '(1 2 3))
  • APPLY- Rufen Sie eine Funktion mit einer Liste als Argument auf : (apply '+ '(1 2 3)).

F: Brauche ich wirklich eine Bewertung oder hat der Compiler / Evaluator bereits das, was ich wirklich will?

Die Hauptgründe EVALfür etwas fortgeschrittenere Benutzer zu vermeiden :

  • Sie möchten sicherstellen, dass Ihr Code kompiliert wird, da der Compiler Code auf viele Probleme überprüfen kann und schnelleren Code generiert, manchmal VIEL VIEL VIEL (das ist Faktor 1000 ;-)) schnellerer Code

  • Code, der erstellt wurde und ausgewertet werden muss, kann nicht so früh wie möglich kompiliert werden.

  • Die Auswertung willkürlicher Benutzereingaben führt zu Sicherheitsproblemen

  • Einige Anwendungen der Evaluierung mit EVALkönnen zur falschen Zeit erfolgen und Build-Probleme verursachen

Um den letzten Punkt mit einem vereinfachten Beispiel zu erklären:

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

Daher möchte ich möglicherweise ein Makro schreiben, das basierend auf dem ersten Parameter entweder SINoder verwendet COS.

(foo 3 4)tut (sin 4)und (foo 1 4)tut (cos 4).

Jetzt haben wir vielleicht:

(foo (+ 2 1) 4)

Dies ergibt nicht das gewünschte Ergebnis.

Man kann dann das Makro reparieren, FOOindem man die Variable bewertet:

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

(foo (+ 2 1) 4)

Aber dann funktioniert das immer noch nicht:

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

Der Wert der Variablen ist zur Kompilierungszeit einfach nicht bekannt.

Ein allgemeiner wichtiger Grund zu vermeiden EVAL: Es wird oft für hässliche Hacks verwendet.

Rainer Joswig
quelle
3
Vielen Dank! Ich habe den letzten Punkt einfach nicht verstanden (Bewertung zum falschen Zeitpunkt?) - Könnten Sie bitte etwas näher darauf eingehen?
Jay
41
+1, da dies die eigentliche Antwort ist - Menschen greifen evaleinfach zurück, weil sie nicht wissen, dass es eine bestimmte Sprach- oder Bibliotheksfunktion gibt, um das zu tun, was sie tun möchten. Ähnliches Beispiel von JS: Ich möchte eine Eigenschaft von einem Objekt unter Verwendung eines dynamischen Namens abrufen, also schreibe ich: eval("obj.+" + propName)wann ich hätte schreiben können obj[propName].
Daniel Earwicker
Ich verstehe, was du jetzt meinst, Rainer! Thansk!
Jay
@ Daniel : "obj.+"? Zuletzt habe ich überprüft, +ist nicht gültig, wenn Punktreferenzen in JS verwendet werden.
Hallo71
2
@ Daniel meinte wahrscheinlich eval ("obj." + PropName), was wie erwartet funktionieren sollte.
Claj
41

eval(in jeder Sprache) ist nicht böse wie eine Kettensäge nicht böse. Es ist ein Werkzeug. Es ist zufällig ein mächtiges Werkzeug, das bei Missbrauch Gliedmaßen durchtrennen und ausweiden kann (bildlich gesprochen), aber das Gleiche gilt für viele Werkzeuge in der Toolbox eines Programmierers, einschließlich:

  • goto und Freunde
  • Lock-basiertes Threading
  • Fortsetzung
  • Makros (hygienisch oder andere)
  • Zeiger
  • neu startbare Ausnahmen
  • selbstmodifizierender Code
  • ... und eine Besetzung von Tausenden.

Wenn Sie eines dieser leistungsstarken, potenziell gefährlichen Tools verwenden müssen, fragen Sie sich dreimal: "Warum?" in einer Kette. Beispielsweise:

"Warum muss ich verwenden eval?" "Wegen foo." "Warum ist foo notwendig?" "Weil ..."

Wenn Sie am Ende dieser Kette angelangt sind und das Werkzeug immer noch so aussieht, als wäre es das Richtige, dann tun Sie es. Dokumentieren Sie die Hölle daraus. Testen Sie die Hölle daraus. Überprüfen Sie die Richtigkeit und Sicherheit immer und immer wieder. Aber mach es.

NUR MEINE RICHTIGE MEINUNG
quelle
Danke - das habe ich schon einmal von eval gehört ("frag dich warum"), aber ich hatte noch nie gehört oder gelesen, was die möglichen Probleme sind. Ich sehe jetzt anhand der Antworten hier, was sie sind (Sicherheits- und Leistungsprobleme).
Jay
8
Und Code-Lesbarkeit. Eval kann den Codefluss völlig durcheinander bringen und ihn unverständlich machen.
NUR MEINE korrekte MEINUNG
Ich verstehe nicht, warum "Lock-basiertes Threading" in Ihrer Liste steht. Es gibt Formen der Parallelität, die keine Sperren beinhalten, und Probleme mit Sperren sind allgemein bekannt, aber ich habe noch nie jemanden gehört, der die Verwendung von Sperren als "böse" bezeichnet.
Asveikau
4
asveikau: Lock-basiertes Threading ist bekanntermaßen schwer zu korrigieren (ich würde vermuten, dass 99,44% des Produktionscodes, der Sperren verwendet, schlecht sind). Es komponiert nicht. Es ist anfällig, Ihren "Multithread" -Code in Seriencode umzuwandeln. (Wenn Sie dies korrigieren, wird der Code nur langsam und aufgebläht.) Es gibt gute Alternativen zum sperrenbasierten Threading, wie z. B. STM- oder Actor-Modelle, die die Verwendung in etwas anderem als dem Code der untersten Ebene böse machen.
Nur meine richtige Meinung
die "Warum-Kette" :) Achten Sie darauf, nach 3 Schritten anzuhalten, es kann weh tun.
Szymanowski
27

Eval ist in Ordnung, solange Sie genau wissen was darin . Alle Benutzereingaben MÜSSEN überprüft und validiert werden und alles. Wenn Sie nicht wissen, wie Sie 100% sicher sein können, dann tun Sie es nicht.

Grundsätzlich kann ein Benutzer einen beliebigen Code für die betreffende Sprache eingeben und dieser wird ausgeführt. Sie können sich vorstellen, wie viel Schaden er anrichten kann.

Tor Valamo
quelle
1
Wenn ich also tatsächlich S-Ausdrücke basierend auf Benutzereingaben mit einem Algorithmus generiere, der Benutzereingaben nicht direkt kopiert, und wenn dies in einer bestimmten Situation einfacher und klarer ist als mit Makros oder anderen Techniken, dann gibt es vermutlich nichts "Böses" " darüber? Mit anderen Worten, die einzigen Probleme bei der Auswertung sind dieselben bei SQL-Abfragen und anderen Techniken, bei denen Benutzereingaben direkt verwendet werden.
Jay
10
Der Grund, warum es "böse" genannt wird, ist, dass es so viel schlimmer ist, es falsch zu machen, als andere Dinge falsch zu machen. Und wie wir wissen, werden Neulinge etwas falsch machen.
Tor Valamo
3
Ich würde nicht sagen, dass der Code validiert werden muss, bevor er unter allen Umständen bewertet wird. Wenn Sie beispielsweise eine einfache REPL implementieren, würden Sie die Eingabe wahrscheinlich nur ungeprüft in eval einspeisen, und das wäre kein Problem (natürlich würden Sie beim Schreiben einer webbasierten REPL eine Sandbox benötigen, aber das ist im Normalfall nicht der Fall CLI-REPLs, die auf dem System des Benutzers ausgeführt werden).
sepp2k
1
Wie ich schon sagte, Sie müssen genau wissen, was passiert, wenn Sie das, was Sie in die Auswertung einspeisen, einspeisen. Wenn dies bedeutet, dass "einige Befehle innerhalb der Grenzen der Sandbox ausgeführt werden", bedeutet dies, dass dies der Fall ist. ;)
Tor Valamo
@ TorValamo jemals von Gefängnispausen gehört?
Loïc Faure-Lacroix
21

"Wann soll ich verwenden eval?" könnte eine bessere Frage sein.

Die kurze Antwort lautet "Wenn Ihr Programm zur Laufzeit ein anderes Programm schreiben und dann ausführen soll". Genetische Programmierung ist ein Beispiel für eine Situation, in der die Verwendung wahrscheinlich sinnvoll ist eval.

Zak
quelle
14

IMO, diese Frage ist nicht spezifisch für LISP . Hier ist eine Antwort auf dieselbe Frage für PHP, die für LISP, Ruby und andere Sprachen gilt, die eine Bewertung haben:

Die Hauptprobleme mit eval () sind:

  • Mögliche unsichere Eingabe. Das Übergeben eines nicht vertrauenswürdigen Parameters kann fehlschlagen. Es ist oft keine triviale Aufgabe, sicherzustellen, dass ein Parameter (oder ein Teil davon) vollständig vertrauenswürdig ist.
  • Trickyness. Die Verwendung von eval () macht Code clever und daher schwieriger zu befolgen. Um Brian Kernighan zu zitieren: "Das Debuggen ist doppelt so schwierig wie das Schreiben des Codes. Wenn Sie den Code also so geschickt wie möglich schreiben, sind Sie per Definition nicht klug genug, um ihn zu debuggen. "

Das Hauptproblem bei der tatsächlichen Verwendung von eval () ist nur eines:

  • unerfahrene Entwickler, die es ohne ausreichende Berücksichtigung verwenden.

Von hier genommen .

Ich denke, das Trickyness-Stück ist ein erstaunlicher Punkt. Die Besessenheit von Code Golf und prägnantem Code hat immer zu "cleverem" Code geführt (für den Auswertungen ein großartiges Werkzeug sind). Aber Sie sollten Ihren Code zur besseren Lesbarkeit schreiben, IMO, um nicht zu demonstrieren, dass Sie ein Schlauer sind, und um kein Papier zu sparen (Sie werden es sowieso nicht drucken).

Dann gibt es in LISP ein Problem im Zusammenhang mit dem Kontext, in dem eval ausgeführt wird, sodass nicht vertrauenswürdiger Code Zugriff auf weitere Dinge erhalten kann. Dieses Problem scheint sowieso häufig zu sein.

Dan Rosenstark
quelle
3
Das Problem der "bösen Eingabe" mit EVAL betrifft nur Nicht-Lisp-Sprachen, da in diesen Sprachen eval () normalerweise ein Zeichenfolgenargument verwendet und die Benutzereingabe normalerweise eingeklebt wird. Der Benutzer kann ein Zitat in seine Eingabe aufnehmen und in diese einsteigen der generierte Code. In Lisp ist das Argument von EVAL jedoch keine Zeichenfolge, und Benutzereingaben können nur dann in den Code gelangen, wenn Sie absolut rücksichtslos sind (wie in der Analyse der Eingabe mit READ-FROM-STRING, um einen S-Ausdruck zu erstellen, den Sie dann einfügen den EVAL-Code, ohne ihn zu zitieren. Wenn Sie ihn zitieren, gibt es keine Möglichkeit, sich dem Zitat zu entziehen.
Werfen Sie Konto weg
12

Es gab viele gute Antworten, aber hier ist eine weitere Einstellung von Matthew Flatt, einem der Implementierer von Racket:

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

Er macht viele der Punkte, die bereits behandelt wurden, aber einige Leute mögen seine Meinung trotzdem interessant finden.

Zusammenfassung: Der Kontext, in dem es verwendet wird, wirkt sich auf das Ergebnis der Bewertung aus, wird jedoch von Programmierern häufig nicht berücksichtigt, was zu unerwarteten Ergebnissen führt.

stchang
quelle
11

Die kanonische Antwort ist, fern zu bleiben. Was ich seltsam finde, weil es ein Primitiv ist, und von den sieben Primitiven (die anderen sind Nachteile, Auto, CDR, If, Gl. Und Zitat) wird es mit Abstand am wenigsten benutzt und geliebt.

Von On Lisp : "Normalerweise ist das explizite Anrufen von eval wie der Kauf von etwas in einem Geschenkeladen am Flughafen. Nachdem Sie bis zum letzten Moment gewartet haben, müssen Sie für eine begrenzte Auswahl zweitklassiger Waren hohe Preise zahlen."

Wann verwende ich eval? Eine normale Verwendung besteht darin, eine REPL in Ihrer REPL durch Auswertung zu haben (loop (print (eval (read)))). Jeder ist damit einverstanden.

Sie können Funktionen aber auch in Form von Makros definieren, die nach der Kompilierung ausgewertet werden, indem Sie eval mit backquote kombinieren. Du gehst

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

und es wird den Kontext für Sie töten.

Swank (für Emacs Slime) ist voll von diesen Fällen. Sie sehen so aus:

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

Ich denke nicht, dass es ein schmutziger Hack ist. Ich benutze es die ganze Zeit selbst, um Makros wieder in Funktionen zu integrieren.

Daniel Cussen
quelle
1
Vielleicht möchten Sie die Kernel-Sprache überprüfen;)
Artemonster
7

Noch ein paar Punkte zu Lisp eval:

  • Es wird in der globalen Umgebung ausgewertet und verliert Ihren lokalen Kontext.
  • Manchmal könnten Sie versucht sein, eval zu verwenden, wenn Sie wirklich das Lesemakro '#' verwenden wollten. welches beim Lesen ausgewertet wird.
pyb
quelle
Ich verstehe, dass die Verwendung von global env sowohl für Common Lisp als auch für Scheme gilt. trifft es auch auf Clojure zu?
Jay
2
In Schema (zumindest für R7RS, möglicherweise auch für R6RS) müssen Sie eine Umgebung an eval übergeben.
CSL
4

Wie die GOTO "Regel": Wenn Sie nicht wissen, was Sie tun, können Sie ein Chaos machen.

Abgesehen davon, dass nur etwas aus bekannten und sicheren Daten erstellt wird, gibt es das Problem, dass einige Sprachen / Implementierungen den Code nicht genug optimieren können. Sie könnten mit interpretiertem Code im Inneren enden eval.

stesch
quelle
Was hat diese Regel mit GOTO zu tun? Gibt es eine Funktion in einer Programmiersprache, mit der Sie nichts anfangen können?
Ken
2
@ Ken: Es gibt keine GOTO-Regel, daher die Anführungszeichen in meiner Antwort. Es gibt nur ein Dogma für Menschen, die Angst haben, für sich selbst zu denken. Gleiches gilt für eval. Ich erinnere mich, wie ich einige Perl-Skripte mithilfe von eval dramatisch beschleunigt habe. Es ist ein Werkzeug in Ihrer Toolbox. Neulinge verwenden eval oft, wenn andere Sprachkonstrukte einfacher / besser sind. Aber es komplett zu vermeiden, nur um cool zu sein und dogmatischen Leuten zu gefallen?
stesch
4

Eval ist einfach unsicher. Zum Beispiel haben Sie folgenden Code:

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

Jetzt kommt der Benutzer zu Ihrer Website und gibt die URL http://example.com/file.php?user= ein ) ein. $ Is_admin = true; echo (

Dann wäre der resultierende Code:

hello();$is_admin=true;echo();
Ragnis
quelle
6
er sprach über Lisp dachte nicht php
fmsf
4
@fmsf Er sprach speziell über Lisp, aber im Allgemeinen evalin jeder Sprache, die es hat.
Skilldrick
4
@fmsf - das ist eigentlich eine sprachunabhängige Frage. Dies gilt sogar für statisch kompilierte Sprachen, da sie die Auswertung simulieren können, indem sie zur Laufzeit den Compiler aufrufen.
Daniel Earwicker
1
In diesem Fall ist die Sprache ein Duplikat. Ich habe hier viele solche gesehen.
FMSF
9
PHP-Bewertung ist nicht wie Lisp-Bewertung. Schauen Sie, es arbeitet mit einer Zeichenfolge, und der Exploit in der URL hängt davon ab, ob Sie eine Textklammer schließen und eine andere öffnen können. Lisp eval ist für solche Dinge nicht anfällig. Sie können Daten auswerten, die als Eingabe aus einem Netzwerk stammen, wenn Sie sie ordnungsgemäß sandboxen (und die Struktur ist einfach genug, um dies zu tun).
Kaz
2

Eval ist nicht böse. Eval ist nicht kompliziert. Es ist eine Funktion, die die Liste zusammenstellt, die Sie an sie übergeben. In den meisten anderen Sprachen würde das Kompilieren von beliebigem Code bedeuten, den AST der Sprache zu lernen und in den Compiler-Interna herumzuwühlen, um die Compiler-API herauszufinden. In lisp nennt man einfach eval.

Wann sollten Sie es verwenden? Wann immer Sie etwas kompilieren müssen, normalerweise ein Programm, das zur Laufzeit beliebigen Code akzeptiert, generiert oder ändert .

Wann solltest du es nicht benutzen? Alle anderen Fälle.

Warum sollten Sie es nicht verwenden, wenn Sie es nicht brauchen? Weil Sie etwas auf unnötig komplizierte Weise tun würden, was zu Problemen bei der Lesbarkeit, Leistung und beim Debuggen führen könnte.

Ja, aber wenn ich ein Anfänger bin, woher weiß ich, ob ich es verwenden soll? Versuchen Sie immer, das, was Sie brauchen, mit Funktionen zu implementieren. Wenn das nicht funktioniert, fügen Sie Makros hinzu. Wenn das immer noch nicht funktioniert, dann bewerten!

Befolgen Sie diese Regeln und Sie werden niemals Böses mit eval tun :)

optevo
quelle
0

Ich mag Zaks Antwort sehr und er hat den Kern der Sache verstanden: eval wird verwendet, wenn Sie eine neue Sprache, ein Skript oder eine Änderung einer Sprache schreiben. Er erklärt nicht weiter, also werde ich ein Beispiel geben:

(eval (read-line))

In diesem einfachen Lisp-Programm wird der Benutzer zur Eingabe aufgefordert, und alles, was er eingibt, wird ausgewertet. Damit dies funktioniert, muss der gesamte Satz von Symboldefinitionen vorhanden sein, wenn das Programm kompiliert wird, da Sie keine Ahnung haben, welche Funktionen der Benutzer eingeben könnte, und Sie alle einschließen müssen. Das heißt, wenn Sie dieses einfache Programm kompilieren, ist die resultierende Binärdatei gigantisch.

Aus diesem Grund können Sie dies grundsätzlich nicht einmal als kompilierbare Aussage betrachten. Sobald Sie eval verwenden , arbeiten Sie im Allgemeinen in einer interpretierten Umgebung, und der Code kann nicht mehr kompiliert werden. Wenn Sie eval nicht verwenden , können Sie ein Lisp- oder Scheme-Programm wie ein C-Programm kompilieren. Daher möchten Sie sicherstellen, dass Sie sich in einer interpretierten Umgebung befinden möchten und müssen, bevor Sie sich zur Verwendung von eval verpflichten .

Tyler Durden
quelle