Mengapa sebenarnya eval evil?

141

Saya tahu bahwa programmer Lisp dan Skema biasanya mengatakan bahwa evalharus dihindari kecuali sangat diperlukan. Saya telah melihat rekomendasi yang sama untuk beberapa bahasa pemrograman, tetapi saya belum melihat daftar argumen yang jelas terhadap penggunaan eval. Di mana saya dapat menemukan akun tentang potensi masalah menggunakan eval?

Sebagai contoh, saya tahu masalah GOTOdalam pemrograman prosedural (membuat program tidak dapat dibaca dan sulit dirawat, membuat masalah keamanan sulit ditemukan, dll), tetapi saya belum pernah melihat argumen yang menentangnya eval.

Menariknya, argumen yang sama terhadap GOTOharus valid terhadap kelanjutan, tetapi saya melihat bahwa Schemers, misalnya, tidak akan mengatakan bahwa kelanjutan adalah "jahat" - Anda harus berhati-hati saat menggunakannya. Mereka lebih cenderung berkerut pada penggunaan kode evaldaripada pada kode menggunakan kelanjutan (sejauh yang saya bisa melihat - saya bisa salah).

Jay
sumber
5
eval bukanlah kejahatan, tetapi kejahatan adalah apa yang eval lakukan
Anurag
9
@yar - Saya pikir komentar Anda menunjukkan pandangan dunia yang sangat mengutus objek-sentris. Ini mungkin berlaku untuk sebagian besar bahasa, tetapi akan berbeda di Common Lisp, di mana metode bukan milik kelas dan bahkan lebih berbeda di Clojure, di mana kelas hanya didukung melalui fungsi interop Java. Jay menandai pertanyaan ini sebagai Skema, yang tidak memiliki gagasan bawaan tentang kelas atau metode (berbagai bentuk OO tersedia sebagai perpustakaan).
Zak
3
@ Zak, Anda benar, saya hanya tahu bahasa yang saya tahu, tetapi bahkan jika Anda bekerja dengan dokumen Word tanpa menggunakan Styles Anda tidak KERING. Maksud saya adalah menggunakan teknologi untuk tidak mengulangi diri Anda sendiri. OO tidak universal, benar ...
Dan Rosenstark
4
Saya mengambil kebebasan untuk menambahkan tag clojure ke pertanyaan ini, karena saya percaya pengguna Clojure akan mendapat manfaat dari paparan jawaban yang sangat baik yang diposting di sini.
Michał Marczyk
... yah, untuk Clojure, setidaknya satu alasan tambahan berlaku: Anda kehilangan kompatibilitas dengan ClojureScript dan turunannya.
Charles Duffy

Jawaban:

148

Ada beberapa alasan mengapa seseorang tidak boleh menggunakannya EVAL.

Alasan utama untuk pemula adalah: Anda tidak membutuhkannya.

Contoh (dengan asumsi Common Lisp):

EVALuasikan ekspresi dengan operator yang berbeda:

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

Itu lebih baik ditulis sebagai:

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

Ada banyak contoh di mana pemula belajar Lisp berpikir mereka butuhkan EVAL, tetapi mereka tidak membutuhkannya - karena ekspresi dievaluasi dan orang juga dapat mengevaluasi bagian fungsi. Sebagian besar waktu penggunaan EVALmenunjukkan kurangnya pemahaman evaluator.

Ini adalah masalah yang sama dengan makro. Seringkali pemula menulis makro, di mana mereka harus menulis fungsi - tidak memahami apa sebenarnya makro dan tidak memahami bahwa fungsi sudah melakukan pekerjaan.

Ini sering merupakan alat yang salah untuk pekerjaan yang harus digunakan EVALdan sering menunjukkan bahwa pemula tidak memahami aturan evaluasi Lisp yang biasa.

Jika Anda merasa perlu EVAL, periksa apakah ada yang suka FUNCALL, REDUCEatau APPLYbisa digunakan.

  • FUNCALL - panggil fungsi dengan argumen: (funcall '+ 1 2 3)
  • REDUCE - panggil fungsi pada daftar nilai dan gabungkan hasilnya: (reduce '+ '(1 2 3))
  • APPLY- memanggil fungsi dengan daftar sebagai argumen: (apply '+ '(1 2 3)).

T: apakah saya benar-benar membutuhkan eval atau apakah kompiler / evaluator sudah seperti yang saya inginkan?

Alasan utama untuk menghindari EVALpengguna yang sedikit lebih maju:

  • Anda ingin memastikan bahwa kode Anda dikompilasi, karena kompiler dapat memeriksa kode untuk banyak masalah dan menghasilkan kode lebih cepat, kadang-kadang JAUH JAUH JAUH (itu faktor 1000 ;-)) kode lebih cepat

  • kode yang dibangun dan perlu dievaluasi tidak dapat dikompilasi sedini mungkin.

  • eval input pengguna sewenang-wenang membuka masalah keamanan

  • beberapa penggunaan evaluasi EVALdapat terjadi pada waktu yang salah dan menimbulkan masalah pembangunan

Untuk menjelaskan poin terakhir dengan contoh sederhana:

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

Jadi, saya mungkin ingin menulis makro yang didasarkan pada parameter pertama menggunakan salah satu SINatau COS.

(foo 3 4)lakukan (sin 4)dan (foo 1 4)lakukan (cos 4).

Sekarang kita mungkin memiliki:

(foo (+ 2 1) 4)

Ini tidak memberikan hasil yang diinginkan.

Seseorang kemudian mungkin ingin memperbaiki makro FOOdengan MENGEVALUASI variabel:

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

(foo (+ 2 1) 4)

Tapi ini masih tidak berhasil:

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

Nilai variabel tidak diketahui pada waktu kompilasi.

Alasan umum yang penting untuk dihindari EVAL: sering digunakan untuk peretasan yang buruk.

Rainer Joswig
sumber
3
Terima kasih! Saya hanya tidak mengerti poin terakhir (evaluasi pada waktu yang salah?) - bisakah Anda sedikit menjelaskan?
Jay
41
Memberi +1 karena ini adalah jawaban yang sebenarnya - orang-orang akan kembali evalhanya karena mereka tidak tahu ada fitur bahasa atau perpustakaan tertentu untuk melakukan apa yang ingin mereka lakukan. Contoh serupa dari JS: Saya ingin mendapatkan properti dari objek menggunakan nama dinamis, jadi saya menulis: eval("obj.+" + propName)ketika saya bisa menulis obj[propName].
Daniel Earwicker
Aku mengerti maksudmu sekarang, Rainer! Bahasa thans!
Jay
@ Daniel: "obj.+"? Terakhir saya periksa, +tidak valid ketika menggunakan referensi-titik di JS.
Hello71
2
@Aniel mungkin berarti eval ("obj." + PropName) yang seharusnya berfungsi seperti yang diharapkan.
claj
41

eval(dalam bahasa apa pun) tidak jahat dengan cara yang sama seperti gergaji mesin yang tidak jahat. Itu adalah alat. Kebetulan itu adalah alat yang ampuh yang, ketika disalahgunakan, dapat memutuskan anggota tubuh dan mengeluarkan isi perut (berbicara secara metaforis), tetapi hal yang sama dapat dikatakan untuk banyak alat di kotak alat programmer termasuk:

  • goto dan teman-teman
  • threading berbasis kunci
  • kelanjutan
  • makro (hygenik atau lainnya)
  • petunjuk
  • pengecualian yang dapat dimulai kembali
  • kode modifikasi diri
  • ... dan para pemain ribuan.

Jika Anda harus menggunakan alat yang kuat dan berpotensi berbahaya ini, tanyakan pada diri sendiri tiga kali "mengapa?" dalam sebuah rantai. Sebagai contoh:

"Kenapa aku harus menggunakan eval?" "Karena foo." "Kenapa foo perlu?" "Karena ..."

Jika Anda sampai ke ujung rantai itu dan alat itu masih terlihat seperti itu adalah hal yang benar untuk dilakukan, maka lakukanlah. Dokumentasikan Neraka keluar dari itu. Uji Neraka keluar dari itu. Periksa kebenaran dan keamanan berulang-ulang. Tapi lakukan itu.

HANYA PENDAPAT SAYA yang benar
sumber
Terima kasih - itulah yang saya dengar tentang eval sebelumnya ("tanyakan pada diri Anda mengapa"), tetapi saya belum pernah mendengar atau membaca apa potensi masalahnya. Saya melihat sekarang dari jawaban di sini apa itu (masalah keamanan dan kinerja).
Jay
8
Dan pembacaan kode. Eval benar-benar dapat mengacaukan aliran kode dan menjadikannya tidak dapat dipahami.
HANYA SAYA PENDAPAT benar
Saya tidak mengerti mengapa "penguncian berbasis kunci" [sic] ada di daftar Anda. Ada beberapa bentuk konkurensi yang tidak melibatkan kunci, dan masalah kunci umumnya diketahui, tetapi saya belum pernah mendengar ada yang menyebut menggunakan kunci sebagai "jahat".
asveikau
4
asveikau: threading berbasis kunci terkenal sulit untuk mendapatkan yang benar (saya kira bahwa 99,44% dari kode produksi menggunakan kunci adalah buruk). Itu tidak menulis. Anda cenderung mengubah kode "multi-utas" menjadi kode seri. (Memperbaiki untuk ini hanya membuat kode lambat dan kembung sebagai gantinya.) Ada alternatif yang baik untuk penguncian berbasis kunci, seperti model STM atau aktor, yang membuatnya menggunakannya dalam apa pun kecuali kode tingkat terendah jahat.
HANYA SAYA PENDAPAT benar
"rantai mengapa" :) pastikan untuk berhenti setelah 3 langkah itu bisa menyakitkan.
szymanowski
27

Eval baik-baik saja, selama Anda tahu PERSIS apa yang masuk ke dalamnya. Setiap input pengguna yang masuk ke dalamnya HARUS diperiksa dan divalidasi dan semuanya. Jika Anda tidak tahu bagaimana menjadi 100% yakin, maka jangan lakukan itu.

Pada dasarnya, pengguna dapat mengetikkan kode apa saja untuk bahasa yang bersangkutan, dan itu akan dijalankan. Anda bisa bayangkan sendiri berapa banyak kerusakan yang bisa dia lakukan.

Tor Valamo
sumber
1
Jadi jika saya benar-benar menghasilkan ekspresi S berdasarkan input pengguna menggunakan algoritma yang tidak akan secara langsung menyalin input pengguna, dan jika itu lebih mudah dan lebih jelas dalam beberapa situasi tertentu daripada menggunakan makro atau teknik lainnya, maka saya kira tidak ada yang "jahat " tentang itu? Dengan kata lain, satu-satunya masalah dengan eval sama dengan query SQL dan teknik lain yang menggunakan input pengguna secara langsung?
Jay
10
Alasan itu disebut "jahat" adalah karena melakukan yang salah jauh lebih buruk daripada melakukan hal-hal lain yang salah. Dan seperti yang kita ketahui, pemula akan melakukan kesalahan.
Tor Valamo
3
Saya tidak akan mengatakan kode itu harus divalidasi sebelum mengubahnya dalam semua keadaan. Misalnya, ketika menerapkan REPL sederhana, Anda mungkin hanya akan memasukkan input ke eval yang tidak dicentang dan itu tidak akan menjadi masalah (tentu saja ketika menulis REPL berbasis web Anda akan memerlukan kotak pasir, tapi itu tidak berlaku untuk normal CLI-REPL yang dijalankan pada sistem pengguna).
sepp2k
1
Seperti saya katakan, Anda harus tahu persis apa yang terjadi ketika Anda memberi makan apa yang Anda makan ke dalam eval. Jika itu berarti "itu akan menjalankan beberapa perintah dalam batas-batas kotak pasir", maka itulah artinya. ;)
Tor Valamo
@TorValamo pernah mendengar tentang hukuman penjara?
Loïc Faure-Lacroix
21

"Kapan saya harus menggunakan eval?" mungkin pertanyaan yang lebih baik.

Jawaban singkatnya adalah "ketika program Anda dimaksudkan untuk menulis program lain saat runtime, dan kemudian jalankan". Pemrograman genetika adalah contoh dari situasi di mana kemungkinan masuk akal untuk digunakan eval.

Zak
sumber
14

IMO, pertanyaan ini tidak spesifik untuk LISP . Berikut ini adalah jawaban untuk pertanyaan yang sama untuk PHP, dan itu berlaku untuk LISP, Ruby, dan bahasa lain yang memiliki eval:

Masalah utama dengan eval () adalah:

  • Input potensial tidak aman. Melewati parameter yang tidak dipercaya adalah cara untuk gagal. Seringkali bukan tugas yang sepele untuk memastikan bahwa suatu parameter (atau bagian darinya) sepenuhnya dipercaya.
  • Trickyness. Menggunakan eval () membuat kode pintar, oleh karena itu lebih sulit untuk diikuti. Mengutip Brian Kernighan " Debugging dua kali lebih keras daripada menulis kode di tempat pertama. Karena itu, jika Anda menulis kode sepintar mungkin, Anda, menurut definisi, tidak cukup pintar untuk men-debug itu "

Masalah utama dengan penggunaan aktual eval () hanya satu:

  • pengembang yang tidak berpengalaman yang menggunakannya tanpa pertimbangan yang cukup.

Diambil dari sini .

Saya pikir tipuan adalah hal yang luar biasa. Obsesi dengan kode golf dan kode ringkas selalu menghasilkan kode "pintar" (yang eval adalah alat yang hebat). Tetapi Anda harus menulis kode Anda agar mudah dibaca, IMO, bukan untuk menunjukkan bahwa Anda cerdas dan tidak menghemat kertas (toh Anda tidak akan mencetaknya).

Kemudian di LISP ada beberapa masalah terkait dengan konteks di mana eval dijalankan, sehingga kode yang tidak dipercaya bisa mendapatkan akses ke lebih banyak hal; masalah ini sepertinya sudah biasa.

Dan Rosenstark
sumber
3
Masalah "input jahat" dengan EVAL hanya memengaruhi bahasa non-Lisp, karena dalam bahasa tersebut, eval () biasanya mengambil argumen string, dan input pengguna biasanya disambungkan. Pengguna dapat memasukkan kutipan dalam input mereka dan melarikan diri ke kode yang dihasilkan. Tetapi dalam Lisp, argumen EVAL bukanlah string, dan input pengguna tidak dapat melarikan diri ke dalam kode kecuali Anda benar-benar gegabah (seperti pada Anda mengurai input dengan READ-FROM-STRING untuk membuat ekspresi S, yang kemudian Anda sertakan dalam kode EVAL tanpa mengutipnya. Jika Anda mengutipnya, tidak ada cara untuk lolos dari kutipan).
Membuang Akun
12

Ada banyak jawaban bagus, tapi inilah yang lain dari Matthew Flatt, salah satu pelaksana Racket:

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

Dia membuat banyak poin yang telah dibahas tetapi beberapa orang mungkin menganggap pendapatnya menarik.

Rangkuman: Konteks di mana ia digunakan mempengaruhi hasil eval tetapi sering tidak dipertimbangkan oleh programmer, yang mengarah ke hasil yang tidak terduga.

stchang
sumber
11

Jawaban kanonik adalah menjauh. Yang saya temukan aneh, karena ini primitif, dan dari tujuh primitif (yang lain adalah kontra, mobil, cdr, jika, eq dan kutipan), semakin jauh menggunakan paling sedikit penggunaan dan cinta.

Dari On Lisp : "Biasanya, memanggil eval secara eksplisit seperti membeli sesuatu di toko suvenir bandara. Setelah menunggu sampai saat terakhir, Anda harus membayar harga tinggi untuk pilihan terbatas barang-barang kelas dua."

Jadi kapan saya menggunakan eval? Satu penggunaan normal adalah memiliki REPL dalam REPL Anda dengan mengevaluasi (loop (print (eval (read)))). Semua orang baik-baik saja dengan penggunaan itu.

Tetapi Anda juga dapat mendefinisikan fungsi dalam hal makro yang akan dievaluasi setelah kompilasi dengan menggabungkan eval dengan backquote. Kamu pergi

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

dan itu akan membunuh konteks untuk Anda.

Swank (untuk slac emacs) penuh dengan kasus ini. Mereka terlihat seperti ini:

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

Saya tidak berpikir itu adalah hack kotor. Saya menggunakannya sepanjang waktu untuk mengintegrasikan kembali fungsi makro.

Daniel Cussen
sumber
1
Anda mungkin ingin memeriksa bahasa kernel;)
artemonster
7

Beberapa poin lain pada Lisp eval:

  • Ini mengevaluasi di bawah lingkungan global, kehilangan konteks lokal Anda.
  • Terkadang Anda mungkin tergoda untuk menggunakan eval, padahal sebenarnya Anda bermaksud menggunakan read-macro '#.' yang mengevaluasi pada waktu baca.
pyb
sumber
Saya mengerti bahwa penggunaan env global berlaku untuk Common Lisp dan Scheme; apakah itu juga berlaku untuk Clojure?
Jay
2
Dalam Skema (setidaknya untuk R7RS, mungkin juga untuk R6RS), Anda harus melewati lingkungan untuk eval.
csl
4

Seperti "aturan" GOTO: Jika Anda tidak tahu apa yang Anda lakukan, Anda dapat membuat kekacauan.

Selain dari hanya membangun sesuatu dari data yang diketahui dan aman, ada masalah bahwa beberapa bahasa / implementasi tidak cukup mengoptimalkan kode. Anda bisa berakhir dengan kode yang ditafsirkan di dalamnya eval.

Stesch
sumber
Apa kaitan aturan itu dengan GOTO? Apakah ada fitur dalam bahasa pemrograman yang Anda tidak bisa membuat kekacauan?
Ken
2
@ Ken: Tidak ada aturan GOTO, maka tanda kutip dalam jawaban saya. Hanya ada dogma untuk orang-orang yang takut untuk berpikir sendiri. Sama untuk eval. Saya ingat mempercepat beberapa skrip Perl secara dramatis dengan menggunakan eval. Ini salah satu alat di kotak alat Anda. Pemula sering menggunakan eval ketika konstruksi bahasa lainnya lebih mudah / lebih baik. Tetapi menghindarinya sepenuhnya hanya untuk menjadi dingin dan menyenangkan orang-orang dogmatis?
stesch
4

Eval tidak aman. Misalnya Anda memiliki kode berikut:

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

Sekarang pengguna datang ke situs Anda dan masuk ke url http://example.com/file.php?user= ); $ is_admin = true; echo (

Maka kode yang dihasilkan adalah:

hello();$is_admin=true;echo();
Ragnis
sumber
6
dia berbicara tentang Lisp yang berpikir bukan php
fmsf
4
@ fmsf Dia berbicara secara khusus tentang Lisp tetapi umumnya tentang evalbahasa apa pun yang memilikinya.
Skilldrick
4
@ fmsf - ini sebenarnya adalah pertanyaan independen-bahasa. Itu bahkan berlaku untuk bahasa yang dikompilasi statis karena mereka dapat mensimulasikan eval dengan memanggil ke kompiler saat runtime.
Daniel Earwicker
1
dalam hal ini bahasanya adalah duplikat. Saya telah melihat banyak seperti ini diseluruh sini.
fmsf
9
PHP eval tidak seperti Lisp eval. Lihat, ini beroperasi pada string karakter, dan exploit dalam URL tergantung pada kemampuan untuk menutup tanda kurung tekstual dan membuka yang lain. Lisp eval tidak rentan terhadap hal semacam ini. Anda dapat mengevaluasi data yang datang sebagai input dari jaringan, jika Anda sandbox dengan benar (dan strukturnya cukup mudah untuk berjalan untuk melakukan itu).
Kaz
2

Eval tidak jahat. Eval tidak rumit. Ini adalah fungsi yang menyusun daftar yang Anda lewati. Di sebagian besar bahasa lain, kompilasi kode arbitrer akan berarti mempelajari AST bahasa dan menggali internal kompiler untuk mengetahui API kompiler. Dalam cadel, Anda hanya memanggil eval.

Kapan sebaiknya Anda menggunakannya? Setiap kali Anda perlu mengkompilasi sesuatu, biasanya sebuah program yang menerima, membuat atau memodifikasi kode arbitrer saat runtime .

Kapan sebaiknya Anda tidak menggunakannya? Semua kasus lainnya.

Mengapa Anda tidak menggunakannya saat Anda tidak perlu? Karena Anda akan melakukan sesuatu dengan cara rumit yang tidak perlu yang dapat menyebabkan masalah keterbacaan, kinerja, dan debugging.

Ya, tetapi jika saya seorang pemula bagaimana saya tahu jika saya harus menggunakannya? Selalu mencoba menerapkan apa yang Anda butuhkan dengan fungsi. Jika itu tidak berhasil, tambahkan makro. Jika itu masih tidak berhasil, maka eval!

Ikuti aturan ini dan Anda tidak akan pernah melakukan kejahatan dengan eval :)

optevo
sumber
0

Saya sangat menyukai jawaban Zak dan dia telah mendapatkan inti dari masalah ini: eval digunakan ketika Anda menulis bahasa baru, naskah, atau modifikasi bahasa. Dia tidak benar-benar menjelaskan lebih lanjut jadi saya akan memberikan contoh:

(eval (read-line))

Dalam program Lisp sederhana ini, pengguna diminta untuk input dan kemudian apa pun yang mereka masukkan dievaluasi. Agar ini bekerja secara keseluruhan rangkaian definisi simbol harus ada jika program dikompilasi, karena Anda tidak tahu fungsi yang dapat dimasukkan oleh pengguna, jadi Anda harus memasukkan semuanya. Itu berarti bahwa jika Anda mengkompilasi program sederhana ini, biner yang dihasilkan akan menjadi raksasa.

Pada prinsipnya, Anda bahkan tidak dapat menganggap ini sebagai pernyataan yang dapat dikompilasi karena alasan ini. Secara umum, setelah Anda menggunakan eval , Anda beroperasi di lingkungan yang ditafsirkan, dan kode tidak lagi dapat dikompilasi. Jika Anda tidak menggunakan eval maka Anda dapat mengkompilasi program Lisp atau Skema seperti program C. Oleh karena itu, Anda ingin memastikan bahwa Anda ingin dan perlu berada dalam lingkungan yang ditafsirkan sebelum berkomitmen untuk menggunakan eval .

Tyler Durden
sumber