Mengapa evaluasi malas tidak digunakan di mana-mana?

32

Saya baru saja belajar cara kerja evaluasi malas dan saya bertanya-tanya: mengapa evaluasi malas tidak diterapkan di setiap perangkat lunak yang saat ini diproduksi? Mengapa masih menggunakan evaluasi yang bersemangat?

John Smith
sumber
2
Berikut adalah contoh dari apa yang bisa terjadi jika Anda mencampur evaluasi yang bisa berubah dan malas. alicebobandmallory.com/articles/2011/01/01/…
Jonas Elfström
2
@ JonasElfström: Tolong, jangan bingung antara keadaan yang bisa berubah dengan salah satu implementasinya. Keadaan yang dapat diubah dapat diimplementasikan menggunakan aliran nilai malas yang tak terbatas. Maka Anda tidak memiliki masalah variabel yang bisa berubah.
Giorgio
Dalam bahasa pemrograman imperatif, "evaluasi malas" membutuhkan upaya sadar dari programmer. Pemrograman generik dalam bahasa imperatif telah membuat ini mudah, tetapi tidak akan pernah transparan. Jawaban ke sisi lain dari pertanyaan tersebut memunculkan pertanyaan lain: "Mengapa bahasa pemrograman fungsional tidak digunakan di mana-mana?", Dan jawaban saat ini hanyalah "tidak" sebagai urusan saat ini.
rwong
2
Bahasa pemrograman fungsional tidak digunakan di mana-mana karena alasan yang sama kita tidak menggunakan palu pada sekrup, tidak setiap masalah dapat dengan mudah diekspresikan dalam input fungsional -> cara keluaran, GUI misalnya lebih cocok untuk diekspresikan dengan cara imperatif .
ALXGTV
Selanjutnya ada dua kelas bahasa pemrograman fungsional (atau setidaknya keduanya mengklaim fungsional), bahasa fungsional imperatif misalnya Clojure, Scala dan deklaratif misalnya Haskell, OCaml.
ALXGTV

Jawaban:

38

Evaluasi malas membutuhkan overhead pembukuan - Anda harus tahu apakah sudah dievaluasi dan hal-hal semacam itu. Evaluasi yang penuh semangat selalu dievaluasi, jadi Anda tidak perlu tahu. Ini terutama benar dalam konteks bersamaan.

Kedua, itu sepele untuk mengubah evaluasi bersemangat menjadi evaluasi malas dengan mengemasnya menjadi objek fungsi untuk dipanggil nanti, jika Anda mau.

Ketiga, evaluasi malas menyiratkan hilangnya kontrol. Bagaimana jika saya malas mengevaluasi membaca file dari disk? Atau mendapatkan waktu? Itu tidak bisa diterima.

Evaluasi yang penuh semangat bisa lebih efisien dan lebih terkendali, dan secara sepele dikonversi menjadi evaluasi yang malas. Mengapa Anda ingin evaluasi malas?

DeadMG
sumber
10
Malas membaca file dari disk sebenarnya benar-benar rapi - untuk sebagian besar program sederhana saya dan skrip, Haskell readFileadalah persis apa yang saya butuhkan. Selain itu, mengubah dari evaluasi malas ke bersemangat sama sepele.
Tikhon Jelvis
3
Setuju dengan Anda semua kecuali paragraf terakhir. Evaluasi malas lebih efisien ketika ada operasi berantai, dan dapat lebih mengontrol kapan Anda benar-benar membutuhkan data
texasbruce
4
Undang-undang functor ingin berbicara dengan Anda tentang "kehilangan kendali". Jika Anda menulis fungsi murni yang beroperasi pada tipe data abadi, evaluasi malas adalah anugerah. Bahasa-bahasa seperti haskell pada dasarnya didasarkan pada konsep kemalasan. Ini rumit di beberapa bahasa, terutama ketika dicampur dengan kode "tidak aman", tetapi Anda membuatnya terdengar seperti lazy berbahaya atau buruk secara default. Itu hanya "berbahaya" dalam kode berbahaya.
sara
1
@DeadMG Tidak jika Anda peduli apakah kode Anda berakhir atau tidak ... Apa yang head [1 ..]memberi Anda dalam bahasa murni yang dievaluasi dengan penuh semangat, karena dalam Haskell itu memberikannya 1?
titik koma
1
Untuk banyak bahasa, menerapkan evaluasi malas setidaknya akan memperkenalkan kompleksitas. Terkadang kompleksitas itu diperlukan, dan memiliki evaluasi yang malas meningkatkan efisiensi secara keseluruhan - terutama jika apa yang sedang dievaluasi hanya diperlukan secara kondisional. Namun, yang dilakukan dengan buruk dapat menyebabkan bug halus atau sulit untuk menjelaskan masalah kinerja karena asumsi buruk saat menulis kode. Ada kompromi.
Berin Loritsch
17

Terutama karena kode dan status malas dapat bercampur dengan buruk dan menyebabkan beberapa bug sulit ditemukan. Jika keadaan objek dependen berubah, nilai objek malas Anda dapat salah saat dievaluasi. Adalah jauh lebih baik untuk memiliki programmer secara eksplisit kode objek menjadi malas ketika dia tahu situasinya tepat.

Di samping catatan Haskell menggunakan evaluasi Malas untuk semuanya. Ini dimungkinkan karena ini adalah bahasa fungsional dan tidak menggunakan status (kecuali dalam beberapa keadaan luar biasa di mana mereka ditandai dengan jelas)

Tom Squires
sumber
Ya, keadaan yang bisa berubah + evaluasi malas = kematian. Saya pikir satu-satunya poin saya kehilangan di final SICP saya adalah tentang menggunakan set!penerjemah Skema malas. > :(
Tikhon Jelvis
3
"kode dan status lazy dapat tercampur dengan buruk": Ini benar-benar tergantung pada bagaimana Anda menerapkan negara. Jika Anda menerapkannya menggunakan variabel yang dapat diubah bersama, dan Anda bergantung pada urutan evaluasi agar negara Anda konsisten, maka Anda benar.
Giorgio
14

Evaluasi malas tidak selalu lebih baik.

Manfaat kinerja evaluasi malas bisa sangat baik, tetapi tidak sulit untuk menghindari evaluasi yang paling tidak perlu di lingkungan yang bersemangat - tentu saja malas membuatnya mudah dan lengkap, tetapi jarang evaluasi yang tidak perlu dalam kode merupakan masalah besar.

Hal yang baik tentang evaluasi malas adalah ketika memungkinkan Anda menulis kode yang lebih jelas; mendapatkan prima ke-10 dengan memfilter daftar bilangan asli tak terbatas dan mengambil elemen ke-10 dari daftar itu adalah salah satu cara yang paling ringkas dan jelas untuk melanjutkan: (pseudocode)

let numbers = [1,2...]
fun is_prime x = none (map (y-> x mod y == 0) [2..x-1])
let primes = filter is_prime numbers
let tenth_prime = first (take primes 10)

Saya percaya itu akan sangat sulit untuk mengekspresikan hal-hal begitu singkat tanpa rasa malas.

Tapi kemalasan bukanlah jawaban untuk segalanya. Sebagai permulaan, kemalasan tidak dapat diterapkan secara transparan di hadapan negara, dan saya percaya keadaan tidak dapat dideteksi secara otomatis (kecuali jika Anda bekerja di katakanlah, Haskell, ketika keadaan cukup eksplisit). Jadi, dalam kebanyakan bahasa, kemalasan perlu dilakukan secara manual, yang membuat segala sesuatunya menjadi kurang jelas dan dengan demikian menghilangkan salah satu manfaat besar dari lazy eval.

Selain itu, kemalasan memiliki kelemahan kinerja, karena menimbulkan overhead signifikan untuk menjaga ekspresi yang tidak dievaluasi; mereka menggunakan penyimpanan dan mereka lebih lambat untuk bekerja dengan nilai-nilai sederhana. Bukan hal yang aneh untuk mengetahui bahwa Anda harus menginginkan kode yang bersemangat karena versi malasnya lambat, dan terkadang sulit untuk beralasan tentang kinerja.

Karena cenderung terjadi, tidak ada strategi terbaik mutlak. Malas sangat bagus jika Anda dapat menulis kode yang lebih baik dengan mengambil keuntungan dari struktur data tak terbatas atau strategi lain yang memungkinkan Anda untuk menggunakannya, tetapi bersemangat bisa lebih mudah untuk dioptimalkan.

alex
sumber
Apakah mungkin bagi kompiler yang benar - benar pintar untuk mengurangi overhead secara signifikan. atau bahkan memanfaatkan kemalasan untuk optimisasi ekstra?
Tikhon Jelvis
3

Berikut ini adalah perbandingan singkat pro dan kontra dari evaluasi bersemangat dan malas:

  • Evaluasi yang bersemangat:

    • Potensi overhead dari hal-hal yang tidak perlu dievaluasi.

    • Tanpa hambatan, evaluasi cepat.

  • Evaluasi malas:

    • Tidak ada evaluasi yang tidak perlu.

    • Pembukuan overhead pada setiap penggunaan suatu nilai.

Jadi, jika Anda memiliki banyak ekspresi yang tidak perlu dievaluasi, malas lebih baik; namun jika Anda tidak pernah memiliki ekspresi yang tidak perlu dievaluasi, malas adalah overhead murni.

Sekarang, mari kita lihat perangkat lunak dunia nyata: Berapa banyak fungsi yang Anda tulis tidak memerlukan evaluasi semua argumen mereka? Apalagi dengan fungsi pendek modern yang hanya melakukan satu hal, persentase fungsi termasuk dalam kategori ini sangat rendah. Dengan demikian, evaluasi yang malas hanya akan memperkenalkan overhead pembukuan sebagian besar waktu, tanpa kesempatan untuk benar-benar menyelamatkan apapun.

Akibatnya, evaluasi malas tidak membayar rata-rata, evaluasi yang penuh semangat lebih cocok untuk kode modern.

cmaster
sumber
1
"Overhead pembukuan pada setiap penggunaan nilai.": Saya tidak berpikir overhead pembukuan lebih besar dari, katakanlah, memeriksa referensi nol dalam bahasa seperti Java. Dalam kedua kasus Anda perlu memeriksa satu bit informasi (dievaluasi / tertunda versus nol / non-nol) dan Anda harus melakukannya setiap kali Anda menggunakan nilai. Jadi, ya, ada overhead, tapi minimal.
Giorgio
1
"Berapa banyak fungsi yang Anda tulis tidak memerlukan evaluasi dari semua argumennya?": Ini hanyalah satu contoh aplikasi. Bagaimana dengan struktur data rekursif dan tak terbatas? Bisakah Anda menerapkannya dengan evaluasi yang penuh semangat? Anda dapat menggunakan iterator, tetapi solusinya tidak selalu singkat. Tentu saja Anda mungkin tidak melewatkan sesuatu yang belum pernah Anda miliki untuk digunakan secara luas.
Giorgio
2
"Konsekuensinya, evaluasi malas tidak membayar rata-rata, evaluasi yang penuh semangat lebih cocok untuk kode modern.": Pernyataan ini tidak berlaku: itu benar-benar tergantung pada apa yang Anda coba implementasikan.
Giorgio
1
@Giorgio Overhead ini mungkin tidak terlihat bagi Anda, tetapi conditional adalah salah satu hal yang menyedot CPU modern: Cabang yang salah duga biasanya memaksa penyiraman saluran pipa yang lengkap, membuang pekerjaan lebih dari sepuluh siklus CPU. Anda tidak ingin kondisi yang tidak perlu di loop batin Anda. Membayar sepuluh siklus ekstra per argumen fungsi hampir tidak dapat diterima untuk kode sensitif kinerja seperti mengkodekan hal itu di java. Anda benar bahwa evaluasi malas memungkinkan Anda melakukan beberapa trik yang tidak dapat Anda lakukan dengan mudah dengan evaluasi yang bersemangat. Tetapi sebagian besar kode tidak memerlukan trik ini.
cmaster
2
Ini sepertinya merupakan jawaban karena kurang pengalaman dengan bahasa dengan evaluasi yang malas. Misalnya, bagaimana dengan struktur data yang tak terbatas?
Andres F.
3

Sebagaimana dicatat oleh @DeadMG, evaluasi Malas membutuhkan biaya pembukuan. Ini bisa mahal relatif terhadap evaluasi yang penuh semangat. Pertimbangkan pernyataan ini:

i = (243 * 414 + 6562 / 435.0 ) ^ 0.5 ** 3

Ini akan membutuhkan sedikit perhitungan untuk menghitung. Jika saya menggunakan evaluasi malas, maka saya perlu memeriksa apakah sudah dievaluasi setiap kali saya menggunakannya. Jika ini di dalam loop ketat yang banyak digunakan maka overhead meningkat secara signifikan, tetapi tidak ada manfaatnya.

Dengan evaluasi yang cepat dan kompiler yang layak, rumus dihitung pada waktu kompilasi. Sebagian besar pengoptimal akan memindahkan tugas dari setiap loop yang terjadi jika sesuai.

Evaluasi malas paling cocok untuk memuat data yang jarang diakses dan memiliki overhead yang tinggi untuk diambil. Oleh karena itu lebih cocok untuk tepi kasus daripada fungsi inti.

Secara umum itu adalah praktik yang baik untuk mengevaluasi hal-hal yang sering diakses sedini mungkin. Evaluasi malas tidak bekerja dengan praktik ini. Jika Anda akan selalu mengakses sesuatu, semua evaluasi malas akan lakukan adalah menambahkan overhead. Biaya / manfaat menggunakan evaluasi malas berkurang karena item yang diakses menjadi kurang mungkin diakses.

Selalu menggunakan evaluasi malas juga menyiratkan optimasi awal. Ini adalah praktik buruk yang sering menghasilkan kode yang jauh lebih kompleks dan mahal yang mungkin terjadi. Sayangnya, optimasi prematur seringkali menghasilkan kode yang kinerjanya lebih lambat daripada kode yang lebih sederhana. Sampai Anda dapat mengukur efek optimasi, adalah ide yang buruk untuk mengoptimalkan kode Anda.

Menghindari optimasi prematur tidak bertentangan dengan praktik pengkodean yang baik. Jika praktik yang baik tidak diterapkan, optimisasi awal dapat terdiri dari penerapan praktik pengkodean yang baik seperti memindahkan perhitungan di luar lingkaran.

BillThor
sumber
1
Anda sepertinya berdebat karena kurang pengalaman. Saya sarankan Anda membaca makalah "Mengapa Pemrograman Fungsional Penting" oleh Wadler. Ini mencurahkan bagian utama menjelaskan mengapa evaluasi malas (petunjuk: itu tidak ada hubungannya dengan kinerja, optimasi awal atau "memuat data yang jarang diakses", dan segala sesuatu yang berkaitan dengan modularitas).
Andres F.
@AndresF Saya sudah membaca makalah yang Anda rujuk. Saya setuju dengan penggunaan evaluasi malas dalam kasus seperti itu. Evaluasi awal mungkin tidak tepat, tetapi saya berpendapat mengembalikan sub-pohon untuk langkah yang dipilih mungkin memiliki manfaat yang signifikan jika langkah tambahan dapat ditambahkan dengan mudah. Namun, membangun fungsionalitas itu bisa menjadi optimasi prematur. Di luar pemrograman fungsional, saya memiliki masalah besar dengan menggunakan evaluasi malas, dan kegagalan untuk menggunakan evaluasi malas. Ada laporan biaya kinerja yang signifikan yang dihasilkan dari evaluasi malas dalam pemrograman fungsional.
BillThor
2
Seperti? Ada laporan biaya kinerja yang signifikan ketika menggunakan evaluasi awal juga (biaya dalam bentuk evaluasi yang tidak dibutuhkan, serta program non-penghentian). Ada biaya untuk hampir semua fitur lain (salah) yang digunakan, kalau dipikir-pikir itu. Modularitas itu sendiri dapat dikenakan biaya; masalahnya adalah apakah itu layak.
Andres F.
3

Jika kita berpotensi harus sepenuhnya mengevaluasi ekspresi untuk menentukan nilainya, maka evaluasi malas dapat merugikan. Katakanlah kita memiliki daftar panjang nilai boolean dan kami ingin mengetahui apakah semuanya benar:

[True, True, True, ... False]

Untuk melakukan ini, kita harus melihat setiap elemen dalam daftar, apa pun yang terjadi, jadi tidak ada kemungkinan untuk memotong evaluasi dengan malas. Kita bisa menggunakan lipatan untuk menentukan apakah semua nilai boolean dalam daftar benar. Jika kita menggunakan hak lipatan, yang menggunakan evaluasi malas, kita tidak mendapatkan manfaat dari evaluasi malas karena kita harus melihat setiap elemen dalam daftar:

foldr (&&) True [True, True, True, ... False] 
> 0.27 secs

Lipatan kanan akan jauh lebih lambat dalam kasus ini daripada lipatan kiri yang ketat, yang tidak menggunakan evaluasi malas:

foldl' (&&) True [True, True, True, ... False] 
> 0.09 secs

Alasannya adalah lipatan ketat kiri menggunakan rekursi ekor, yang berarti ia mengakumulasi nilai kembali dan tidak membangun dan menyimpan dalam rantai operasi besar memori. Ini jauh lebih cepat daripada lazy fold karena kedua fungsi tetap harus melihat seluruh daftar dan flip kanan tidak dapat menggunakan rekursi ekor. Jadi, intinya adalah, Anda harus menggunakan apa pun yang terbaik untuk tugas yang ada.

tail_recursion
sumber
"Jadi, intinya adalah, kamu harus menggunakan apa pun yang terbaik untuk tugas yang ada." +1
Giorgio