Dlaczego dokładnie jest Eval Eval?

141

Wiem, że programiści Lisp i Scheme zwykle mówią, że evalnależy tego unikać, chyba że jest to absolutnie konieczne. Widziałem to samo zalecenie dla kilku języków programowania, ale nie widziałem jeszcze listy jasnych argumentów przeciwko używaniu eval. Gdzie mogę znaleźć opis potencjalnych problemów z używaniem eval?

Na przykład znam problemy związane z GOTOprogramowaniem proceduralnym (sprawia, że ​​programy są nieczytelne i trudne w utrzymaniu, utrudniają znalezienie problemów z bezpieczeństwem itp.), Ale nigdy nie widziałem argumentów przeciwko eval.

Co ciekawe, te same argumenty przeciwko GOTOkontynuacjom powinny być ważne, ale widzę, że na przykład Schemerzy nie powiedzą, że kontynuacje są „złe” - należy po prostu zachować ostrożność podczas ich używania. Są znacznie bardziej skłonni do marszczenia się na używaniu kodu evalniż na kod używający kontynuacji (o ile widzę - mogę się mylić).

Sójka
źródło
5
eval nie jest złem, ale zło robi eval
Anurag
9
@yar - myślę, że twój komentarz wskazuje na światopogląd skoncentrowany na pojedynczych wysyłkach. Prawdopodobnie jest to poprawne dla większości języków, ale byłoby inne w Common Lisp, gdzie metody nie należą do klas, a jeszcze bardziej w Clojure, gdzie klasy są obsługiwane tylko przez funkcje współpracy Java. Jay oznaczył to pytanie jako Schemat, który nie ma żadnego wbudowanego pojęcia klas lub metod (różne formy obiektów obiektowych są dostępne jako biblioteki).
Zak
3
@Zak, masz rację, znam tylko języki, które znam, ale nawet jeśli pracujesz z dokumentem Worda bez używania stylów, nie jesteś SUCHY. Chodziło mi o to, aby użyć technologii, aby się nie powtarzać. OO nie jest uniwersalne, prawda ...
Dan Rosenstark
4
Pozwoliłem sobie na dodanie tagu clojure do tego pytania, ponieważ uważam, że użytkownicy Clojure mogą skorzystać z dostępu do doskonałych odpowiedzi zamieszczonych tutaj.
Michał Marczyk
... cóż, dla Clojure ma zastosowanie co najmniej jeden dodatkowy powód: tracisz zgodność z ClojureScript i jego pochodnymi.
Charles Duffy

Odpowiedzi:

148

Istnieje kilka powodów, dla których nie powinno się ich używać EVAL.

Głównym powodem dla początkujących jest: nie potrzebujesz tego.

Przykład (zakładając Common Lisp):

OCENA wyrażenia za pomocą różnych operatorów:

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

To lepiej napisać jako:

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

Istnieje wiele przykładów, w których początkujący uczący się Lispa myślą, że potrzebują EVAL, ale nie potrzebują tego - ponieważ wyrażenia są oceniane i można również ocenić część funkcji. W większości przypadków użycie EVALznaku wskazuje na brak zrozumienia ewaluatora.

Ten sam problem dotyczy makr. Często początkujący piszą makra, w których powinni pisać funkcje - nie rozumiejąc, do czego naprawdę służą makra i nie rozumiejąc, że funkcja już wykonuje swoje zadanie.

Często jest to niewłaściwe narzędzie do pracy EVALi często wskazuje, że początkujący nie rozumie zwykłych zasad oceny Lisp.

Jeśli uważasz, że potrzebujesz EVAL, sprawdź FUNCALL, REDUCEczy APPLYzamiast tego można użyć czegoś takiego lub .

  • FUNCALL - wywołaj funkcję z argumentami: (funcall '+ 1 2 3)
  • REDUCE - wywołaj funkcję na liście wartości i połącz wyniki: (reduce '+ '(1 2 3))
  • APPLY- wywołać funkcję z listy jako argumentów: (apply '+ '(1 2 3)).

P: Czy naprawdę potrzebuję eval, czy też kompilator / oceniający już to, czego naprawdę chcę?

Główne powody, których należy unikać w EVALprzypadku nieco bardziej zaawansowanych użytkowników:

  • chcesz mieć pewność, że Twój kod jest skompilowany, ponieważ kompilator może sprawdzić kod pod kątem wielu problemów i generuje szybszy kod, czasami DUŻO DUŻO (to współczynnik 1000 ;-)) szybszy kod

  • kod, który jest skonstruowany i musi zostać oceniony, nie może zostać skompilowany tak wcześnie, jak to możliwe.

  • ocena arbitralnych danych wejściowych użytkownika stwarza problemy związane z bezpieczeństwem

  • niektóre zastosowania ewaluacji EVALmogą się wydarzyć w złym czasie i spowodować problemy z kompilacją

Aby wyjaśnić ostatni punkt za pomocą uproszczonego przykładu:

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

Więc może zechcę napisać makro, które na podstawie pierwszego parametru używa albo SINlub COS.

(foo 3 4)robi (sin 4)i (foo 1 4)robi (cos 4).

Teraz możemy mieć:

(foo (+ 2 1) 4)

Nie daje to pożądanego rezultatu.

Wtedy można chcieć naprawić makro FOO, oceniając zmienną:

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

(foo (+ 2 1) 4)

Ale to nadal nie działa:

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

Wartość zmiennej po prostu nie jest znana w czasie kompilacji.

Ogólny ważny powód, którego należy unikać EVAL: jest często używany do brzydkich hacków.

Rainer Joswig
źródło
3
Dzięki! Po prostu nie zrozumiałem ostatniego punktu (ocena w złym czasie?) - czy możesz trochę rozwinąć, proszę?
Jay
41
+1, ponieważ to jest prawdziwa odpowiedź - ludzie wracają po evalprostu dlatego, że nie wiedzą, że istnieje konkretny język lub funkcja biblioteki, aby robić to, co chcą. Podobny przykład z JS: Chcę uzyskać właściwość z obiektu za pomocą dynamicznej nazwy, więc piszę: eval("obj.+" + propName)kiedy mogłem napisać obj[propName].
Daniel Earwicker
Teraz rozumiem, co masz na myśli, Rainer! Thansk!
Jay
@Daniel: "obj.+"? Ostatnio sprawdzałem, +nie działa, gdy używam odwołań kropkowych w JS.
Hello71
2
@Daniel prawdopodobnie miał na myśli eval ("obj." + Nazwa_właściwości), który powinien działać zgodnie z oczekiwaniami.
claj
41

eval(w każdym języku) nie jest zła w taki sam sposób, w jaki piła łańcuchowa nie jest zła. To jest narzędzie. Okazuje się, że jest to potężne narzędzie, które w przypadku niewłaściwego użycia może odciąć kończyny i wypatroszyć (mówiąc metaforycznie), ale to samo można powiedzieć o wielu narzędziach w zestawie narzędzi programisty, w tym:

  • goto i przyjaciele
  • gwintowanie oparte na blokadzie
  • kontynuacje
  • makra (higieniczne lub inne)
  • wskaźniki
  • wyjątki z możliwością ponownego uruchomienia
  • samomodyfikujący się kod
  • ... i obsada tysięcy.

Jeśli zauważysz, że musisz użyć któregokolwiek z tych potężnych, potencjalnie niebezpiecznych narzędzi, trzy razy zadaj sobie pytanie „dlaczego?” w łańcuchu. Na przykład:

„Dlaczego muszę używać eval?” "Z powodu foo." "Dlaczego foo jest konieczne?" "Ponieważ ..."

Jeśli dojdziesz do końca tego łańcucha, a narzędzie nadal wygląda tak, jakby było to właściwe, zrób to. Udokumentuj z tego piekło. Wypróbujcie z tego piekło. Wielokrotnie sprawdzaj poprawność i bezpieczeństwo. Ale zrób to.

TYLKO MOJA poprawna OPINIA
źródło
Dzięki - o tym słyszałem już wcześniej („zadaj sobie pytanie dlaczego”), ale nigdy jeszcze nie słyszałem ani nie czytałem, jakie są potencjalne problemy. Na podstawie odpowiedzi widzę teraz, jakie one są (problemy z bezpieczeństwem i wydajnością).
Jay
8
I czytelność kodu. Eval może całkowicie zepsuć przepływ kodu i uczynić go niezrozumiałym.
TYLKO MOJA poprawna OPINIA
Nie rozumiem, dlaczego „wątkowanie oparte na blokadach” [sic] znajduje się na twojej liście. Istnieją formy współbieżności, które nie obejmują blokad, a problemy z zamkami są ogólnie dobrze znane, ale nigdy nie słyszałem, aby ktokolwiek opisywał używanie zamków jako „zło”.
asveikau
4
asveikau: Wątki oparte na blokadach są bardzo trudne do wykonania (domyślam się, że 99,44% kodu produkcyjnego używającego blokad jest złych). Nie komponuje się. Jest podatny na przekształcanie kodu „wielowątkowego” w kod seryjny. (Poprawienie tego po prostu powoduje, że kod jest powolny i zamiast tego rozdęty). Istnieją dobre alternatywy dla wątków opartych na blokadach, takie jak modele STM lub aktorów, które sprawiają, że użycie go w każdym kodzie niż najniższego poziomu jest złe.
TYLKO MOJA poprawna OPINIA
"dlaczego łańcuch" :) pamiętaj, aby zatrzymać się po 3 krokach, może to boleć.
szymanowski
27

Eval jest w porządku, o ile dokładnie wiesz, co się z nim dzieje. Każde wprowadzane przez użytkownika dane wejściowe MUSZĄ zostać sprawdzone i zatwierdzone i wszystko. Jeśli nie wiesz, jak być w 100% pewnym, nie rób tego.

Zasadniczo użytkownik może wpisać dowolny kod dla danego języka i zostanie on wykonany. Możesz sobie wyobrazić, ile szkód może wyrządzić.

Tor Valamo
źródło
1
Więc jeśli faktycznie generuję wyrażenia S na podstawie danych wejściowych użytkownika przy użyciu algorytmu, który nie kopiuje bezpośrednio danych wejściowych użytkownika, i jeśli jest to łatwiejsze i bardziej przejrzyste w jakiejś konkretnej sytuacji niż użycie makr lub innych technik, to przypuszczam, że nie ma nic "złego " o tym? Innymi słowy, jedyne problemy z eval są takie same w przypadku zapytań SQL i innych technik, które bezpośrednio wykorzystują dane wejściowe użytkownika?
Jay
10
Powodem, dla którego nazywa się to „złem”, jest to, że robienie tego źle jest o wiele gorsze niż robienie innych rzeczy źle. A jak wiemy, nowicjusze będą robić rzeczy źle.
Tor Valamo,
3
Nie powiedziałbym, że kod musi zostać zweryfikowany przed oceną go w każdych okolicznościach. Na przykład, implementując prostą REPL, prawdopodobnie po prostu wprowadzisz dane wejściowe do eval niezaznaczone, co nie byłoby problemu (oczywiście podczas pisania REPL opartego na sieci WWW potrzebowałbyś piaskownicy, ale tak nie jest w przypadku normalnego CLI-REPL, które działają w systemie użytkownika).
sepp2k
1
Jak powiedziałem, musisz dokładnie wiedzieć, co się dzieje, gdy karmisz eval tym, czym się karmisz. Jeśli to oznacza „wykona niektóre polecenia w granicach piaskownicy”, to właśnie to oznacza. ;)
Tor Valamo
@TorValamo słyszałeś kiedyś o ucieczce z więzienia?
Loïc Faure-Lacroix,
21

„Kiedy używać eval?” mogłoby być lepszym pytaniem.

Krótka odpowiedź brzmi: „kiedy twój program ma napisać inny program w czasie wykonywania, a następnie go uruchomić”. Programowanie genetyczne jest przykładem sytuacji, w której prawdopodobnie ma sens eval.

Zak
źródło
14

IMO, to pytanie nie jest specyficzne dla LISP . Oto odpowiedź na to samo pytanie dla PHP i dotyczy to LISP, Ruby i innych języków, które mają wartość eval:

Główne problemy z eval () to:

  • Potencjalne niebezpieczne wejście. Przekazanie niezaufanego parametru jest sposobem na niepowodzenie. Często upewnienie się, że parametr (lub jego część) jest w pełni zaufany, nie jest łatwym zadaniem.
  • Podstępność. Użycie eval () sprawia, że ​​kod jest sprytny, a przez to trudniejszy do śledzenia. Cytując Briana Kernighana: „ Debugowanie jest dwa razy trudniejsze niż pisanie kodu. Dlatego jeśli piszesz kod tak sprytnie, jak to możliwe, z definicji nie jesteś wystarczająco inteligentny, aby go debugować

Główny problem z faktycznym użyciem eval () jest tylko jeden:

  • niedoświadczeni programiści, którzy używają go bez odpowiedniego namysłu.

Zaczerpnięte stąd .

Myślę, że ten podstępny kawałek jest niesamowity. Obsesja na punkcie kodu golfowego i zwięzłego kodu zawsze skutkowała „sprytnym” kodem (dla którego evals są świetnym narzędziem). Ale powinieneś napisać swój kod pod kątem czytelności, IMO, nie po to, aby pokazać, że jesteś głupcem i nie oszczędzać papieru (i tak go nie będziesz drukować).

Następnie w LISP-ie występuje problem związany z kontekstem, w którym uruchamiany jest eval, więc niezaufany kod może uzyskać dostęp do większej liczby rzeczy; i tak wydaje się, że ten problem jest powszechny.

Dan Rosenstark
źródło
3
Problem "złych danych wejściowych" z EVAL dotyczy tylko języków innych niż Lisp, ponieważ w tych językach eval () zazwyczaj pobiera argument typu string, a dane wejściowe użytkownika są zwykle łączone. Użytkownik może dołączyć cytat do swojego wejścia i uciekać do wygenerowany kod. Ale w Lispie argument EVAL nie jest łańcuchem, a dane wejściowe użytkownika nie mogą uciec do kodu, chyba że jesteś absolutnie lekkomyślny (tak jak podczas analizowania danych wejściowych za pomocą READ-FROM-STRING, aby utworzyć S-wyrażenie, które następnie umieścisz w kod EVAL bez cytowania go. Jeśli go zacytujesz, nie ma sposobu na uniknięcie cudzysłowu).
Wyrzuć konto
12

Było wiele świetnych odpowiedzi, ale oto kolejne spojrzenie Matthew Flatta, jednego z twórców Racketa:

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

Przedstawia wiele kwestii, które zostały już omówione, ale niektórzy ludzie mogą mimo to uznać jego podejście za interesujące.

Podsumowanie: Kontekst, w którym jest używany, wpływa na wynik eval, ale często nie jest brany pod uwagę przez programistów, co prowadzi do nieoczekiwanych wyników.

stchang
źródło
11

Kanoniczna odpowiedź brzmi: trzymaj się z daleka. Co wydaje mi się dziwne, ponieważ jest prymitywne, a spośród siedmiu prymitywów (pozostałe to minusy, samochód, cdr, if, eq i cytat), jest ono bardzo użyteczne i najmniej miłe.

From On Lisp : „Zwykle wyraźne dzwonienie do eval jest jak kupowanie czegoś w sklepie z pamiątkami na lotnisku. Czekając do ostatniej chwili, trzeba zapłacić wysokie ceny za ograniczony wybór towarów drugorzędnych”.

Kiedy więc używać eval? Jednym z normalnych zastosowań jest posiadanie REPL w REPL poprzez ocenę (loop (print (eval (read)))). Każdy jest w porządku z tym użyciem.

Ale możesz także zdefiniować funkcje w postaci makr, które będą oceniane po kompilacji, łącząc eval z odwrotnym cudzysłowem. Ty idź

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

i zabije dla ciebie kontekst.

Swank (dla szlamu emacsa) jest pełen takich przypadków. Wyglądają tak:

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

Nie sądzę, że to brudny hack. Sam używam go cały czas do ponownego zintegrowania makr w funkcje.

Daniel Cussen
źródło
1
Możesz sprawdzić język kernela;)
artemonster
7

Kolejne punkty dotyczące ewaluacji Lispa:

  • Ocenia w środowisku globalnym, tracąc lokalny kontekst.
  • Czasami możesz ulec pokusie użycia eval, kiedy naprawdę chciałeś użyć makra odczytu „#”. która jest oceniana w czasie odczytu.
pyb
źródło
Rozumiem, że użycie globalnego środowiska env jest prawdziwe zarówno w przypadku Common Lisp, jak i Scheme; czy dotyczy to również Clojure?
Jay
2
W Scheme (przynajmniej dla R7RS, być może także dla R6RS) musisz przekazać środowisko do eval.
csl
4

Tak jak „zasada” GOTO: jeśli nie wiesz, co robisz, możesz narobić bałaganu.

Oprócz budowania czegoś ze znanych i bezpiecznych danych istnieje problem polegający na tym, że niektóre języki / implementacje nie mogą wystarczająco zoptymalizować kodu. Możesz skończyć z zinterpretowanym kodem w środku eval.

smród
źródło
Co ta reguła ma wspólnego z GOTO? Czy jest jakaś funkcja w jakimkolwiek języku programowania, z którą nie możesz narobić bałaganu?
Ken
2
@Ken: Nie ma reguły GOTO, stąd cudzysłów w mojej odpowiedzi. Jest tylko dogmat dla ludzi, którzy boją się myśleć samodzielnie. To samo dotyczy eval. Pamiętam, jak dramatycznie przyspieszyłem niektóre skrypty Perla za pomocą eval. To jedno narzędzie w Twoim zestawie narzędzi. Początkujący często używają eval, gdy inne konstrukcje językowe są łatwiejsze / lepsze. Ale unikanie tego całkowicie tylko po to, by być fajnym i zadowolić dogmatycznych ludzi?
stesz
4

Eval jest po prostu niepewny. Na przykład masz następujący kod:

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

Teraz użytkownik przychodzi do Twojej witryny i wpisuje adres URL http://example.com/file.php?user= ); $ is_admin = true; echo (

Wtedy wynikowy kod wyglądałby tak:

hello();$is_admin=true;echo();
Ragnis
źródło
6
mówił o Lisp, nie myślałem, że php
fmsf
4
@fmsf Mówił konkretnie o Lispie, ale ogólnie o evalkażdym języku, który go posiada.
Skilldrick
4
@fmsf - to jest właściwie pytanie niezależne od języka. Dotyczy to nawet statycznych języków kompilowanych, ponieważ mogą one symulować eval, wywołując kompilator w czasie wykonywania.
Daniel Earwicker
1
w takim przypadku język jest duplikatem. Widziałem wiele takich tutaj.
fmsf
9
PHP eval różni się od Lisp eval. Spójrz, działa na ciągu znaków, a exploit w adresie URL zależy od możliwości zamknięcia nawiasu tekstowego i otwarcia kolejnego. Lisp eval nie jest podatny na tego typu rzeczy. Możesz oceniać dane przychodzące jako dane wejściowe z sieci, jeśli odpowiednio je piaskowujesz (a struktura jest wystarczająco łatwa do przejścia, aby to zrobić).
Kaz
2

Eval nie jest zły. Eval nie jest skomplikowany. Jest to funkcja, która kompiluje listę, którą do niej przekazujesz. W większości innych języków kompilacja dowolnego kodu oznaczałaby naukę języka AST i przeszukiwanie wewnętrznych elementów kompilatora, aby znaleźć jego API. Podczas seplenienia po prostu dzwonisz do eval.

Kiedy należy go używać? Ilekroć musisz coś skompilować, zazwyczaj jest to program, który akceptuje, generuje lub modyfikuje dowolny kod w czasie wykonywania .

Kiedy nie należy go używać? Wszystkie inne przypadki.

Dlaczego nie powinieneś go używać, kiedy nie musisz? Ponieważ robiłbyś coś w niepotrzebnie skomplikowany sposób, który może powodować problemy z czytelnością, wydajnością i debugowaniem.

Tak, ale jeśli jestem początkującym, skąd mam wiedzieć, czy powinienem go używać? Zawsze staraj się zaimplementować to, czego potrzebujesz, za pomocą funkcji. Jeśli to nie zadziała, dodaj makra. Jeśli to nadal nie zadziała, eval!

Przestrzegaj tych zasad, a z eval nigdy nie zrobisz zła :)

optevo
źródło
0

Bardzo podoba mi się odpowiedź Zaka i on dotarł do istoty sprawy: eval jest używany, gdy piszesz nowy język, skrypt lub modyfikujesz język. Nie wyjaśnia dalej, więc podam przykład:

(eval (read-line))

W tym prostym programie Lisp użytkownik jest proszony o wprowadzenie danych, a następnie oceniane jest wszystko, co wprowadzi. Aby to zadziałało, cały zestaw definicji symboli musi być obecny, jeśli program jest kompilowany, ponieważ nie masz pojęcia, które funkcje użytkownik może wprowadzić, więc musisz uwzględnić je wszystkie. Oznacza to, że jeśli skompilujesz ten prosty program, wynikowy plik binarny będzie gigantyczny.

Z tego powodu nie można nawet uznać tego oświadczenia za kompilowalny. Ogólnie rzecz biorąc, gdy używasz eval , pracujesz w zinterpretowanym środowisku, a kodu nie można już kompilować. Jeśli nie używasz eval , możesz skompilować program Lisp lub Scheme, tak jak program C. Dlatego chcesz się upewnić, że chcesz i musisz być w interpretowanym środowisku, zanim zdecydujesz się na użycie eval .

Tyler Durden
źródło