Tampaknya evaluasi ekspresi yang malas dapat menyebabkan seorang programmer kehilangan kendali atas urutan di mana kode mereka dieksekusi. Saya mengalami kesulitan memahami mengapa ini bisa diterima atau diinginkan oleh seorang programmer.
Bagaimana paradigma ini digunakan untuk membangun perangkat lunak yang dapat diprediksi yang berfungsi sebagaimana dimaksud, ketika kami tidak memiliki jaminan kapan dan di mana ekspresi akan dievaluasi?
functional-programming
haskell
akashchandrakar
sumber
sumber
head . sort
memilikiO(n)
kompleksitas karena kemalasan (tidakO(n log n)
). Lihat Evaluasi Malas dan Kompleksitas Waktu .Jawaban:
Banyak jawaban yang masuk ke hal-hal seperti daftar tak terbatas dan keuntungan kinerja dari bagian perhitungan yang tidak dievaluasi, tetapi ini hilang motivasi yang lebih besar untuk kemalasan: modularitas .
Argumen klasik dituangkan dalam makalah yang banyak dikutip "Mengapa Pemrograman Fungsional Penting" (tautan PDF) oleh John Hughes. Contoh utama dalam makalah itu (Bagian 5) adalah bermain Tic-Tac-Toe menggunakan algoritma pencarian alpha-beta. Kuncinya adalah (hlm. 9):
Program Tic-Tac-Toe dapat ditulis sebagai fungsi yang menghasilkan seluruh pohon permainan mulai dari posisi tertentu, dan fungsi terpisah yang mengkonsumsinya. Pada saat runtime ini tidak secara intrinsik menghasilkan seluruh pohon permainan, hanya sub-bagian yang benar-benar dibutuhkan konsumen. Kita dapat mengubah urutan dan kombinasi di mana alternatif diproduksi dengan mengubah konsumen; tidak perlu mengganti generator sama sekali.
Dalam bahasa yang bersemangat, Anda tidak dapat menulis dengan cara ini karena Anda mungkin akan menghabiskan terlalu banyak waktu dan memori menghasilkan pohon. Jadi Anda akhirnya:
sumber
Ketika ekspresi bebas efek samping, urutan ekspresi dievaluasi tidak mempengaruhi nilainya, sehingga perilaku program tidak terpengaruh oleh urutan. Jadi perilakunya bisa diprediksi dengan sempurna.
Sekarang efek samping adalah masalah yang berbeda. Jika efek samping dapat terjadi dalam urutan apa pun, perilaku program memang tidak dapat diprediksi. Tapi ini sebenarnya bukan masalahnya. Bahasa malas seperti Haskell menjadikannya titik referensi transparan, yaitu memastikan bahwa urutan ekspresi dievaluasi tidak akan pernah mempengaruhi hasil mereka. Dalam Haskell ini dicapai dengan memaksa semua operasi dengan efek samping yang terlihat pengguna terjadi di dalam IO monad. Ini memastikan bahwa semua efek samping terjadi tepat sesuai urutan yang Anda harapkan.
sumber
Jika Anda terbiasa dengan database, cara yang sangat sering untuk memproses data adalah:
select * from foobar
Anda tidak mengontrol bagaimana hasil dihasilkan dan dengan cara apa (indeks? Pemindaian tabel lengkap?), Atau kapan (haruskah semua data dihasilkan sekaligus atau secara bertahap saat diminta?). Yang Anda tahu adalah: jika ada lebih banyak data, Anda akan mendapatkannya saat Anda memintanya.
Evaluasi malas cukup dekat dengan hal yang sama. Katakanlah Anda memiliki daftar tak terbatas yang didefinisikan sebagai. urutan Fibonacci - jika Anda membutuhkan lima angka, Anda mendapatkan lima angka yang dihitung; jika Anda membutuhkan 1000, Anda mendapatkan 1000. Triknya adalah bahwa runtime tahu apa yang harus disediakan di mana dan kapan. Ini sangat, sangat berguna.
(Pemrogram Java dapat meniru perilaku ini dengan Iterator - bahasa lain mungkin memiliki sesuatu yang serupa)
sumber
Collection2.filter()
(serta metode lain dari kelas itu) cukup banyak menerapkan evaluasi malas: hasilnya "terlihat seperti" biasaCollection
, tetapi urutan pelaksanaannya mungkin tidak intuitif (atau setidaknya tidak jelas). Juga, adayield
dalam Python (dan fitur serupa di C #, yang saya tidak ingat nama) yang bahkan lebih dekat untuk mendukung evaluasi malas daripada Iterator normal.Pertimbangkan untuk menanyakan daftar 2000 pengguna pertama di database Anda yang namanya dimulai dengan "Ab" dan lebih dari 20 tahun. Mereka juga laki-laki.
Ini diagram kecil.
Seperti yang dapat Anda lihat dari interaksi mengerikan yang mengerikan ini, "database" tidak benar-benar melakukan apa pun sampai siap untuk menangani semua kondisi. Ini memuat hasil malas di setiap langkah dan menerapkan kondisi baru setiap kali.
Bertentangan dengan mendapatkan 2000 pengguna pertama, mengembalikan mereka, memfilter mereka untuk "Ab", mengembalikan mereka, memfilter mereka selama lebih dari 20, mengembalikan mereka, dan memfilter untuk laki-laki dan akhirnya mengembalikan mereka.
Singkatnya memuat secara singkat.
sumber
Perancang seharusnya tidak peduli dengan urutan ekspresi dievaluasi asalkan hasilnya sama. Dengan menunda evaluasi, dimungkinkan untuk menghindari evaluasi beberapa ekspresi sekaligus, yang menghemat waktu.
Anda dapat melihat ide yang sama bekerja di tingkat yang lebih rendah: banyak mikroprosesor dapat menjalankan instruksi yang tidak sesuai pesanan, yang memungkinkan mereka menggunakan berbagai unit eksekusi mereka dengan lebih efisien. Kuncinya adalah mereka melihat ketergantungan antara instruksi dan menghindari menyusun ulang di mana itu akan mengubah hasilnya.
sumber
Ada beberapa argumen untuk evaluasi malas yang menurut saya menarik
Modularitas Dengan evaluasi malas Anda dapat memecah kode menjadi beberapa bagian. Sebagai contoh, misalkan Anda memiliki masalah untuk "menemukan sepuluh balasan pertama elemen dalam daftar daftar sedemikian sehingga timbal balik kurang dari 1." Dalam sesuatu seperti Haskell, Anda dapat menulis
tetapi ini hanya salah dalam bahasa yang ketat, karena jika Anda memberikannya
[2,3,4,5,6,7,8,9,10,11,12,0]
Anda akan membaginya dengan nol. Lihat jawaban sacundim untuk alasan mengapa hal ini mengagumkan dalam praktikLebih banyak hal yang bekerja dengan ketat (pun intended) lebih banyak program berakhir dengan evaluasi yang tidak ketat dibandingkan dengan evaluasi yang ketat. Jika program Anda diakhiri dengan strategi evaluasi "bersemangat" itu akan berakhir dengan yang "malas", tetapi kebalikannya tidak benar. Anda mendapatkan hal-hal seperti struktur data tak terbatas (yang sebenarnya hanya agak keren) sebagai contoh spesifik dari fenomena ini. Lebih banyak program bekerja dalam bahasa yang malas.
Optimalitas Evaluasi panggilan-oleh-kebutuhan secara asimptotik optimal sehubungan dengan waktu. Meskipun bahasa-bahasa malas utama (yang pada dasarnya adalah Haskell dan Haskell) tidak menjanjikan panggilan-oleh-kebutuhan, Anda dapat lebih atau kurang mengharapkan model biaya yang optimal. Analisis keketatan (dan evaluasi spekulatif) menjaga overhead dalam praktik. Ruang adalah masalah yang lebih rumit.
Pasukan Purity menggunakan evaluasi malas membuat berurusan dengan efek samping dengan cara yang tidak disiplin total rasa sakit, karena seperti yang Anda katakan, programmer kehilangan kendali. Ini hal yang baik. Transparansi referensial membuat pemrograman, pembiasan ulang, dan penalaran tentang program jauh lebih mudah. Bahasa-bahasa yang ketat pasti akan menyerah pada tekanan karena memiliki bit-bit yang tidak murni - sesuatu yang Haskell dan Clean telah tolak dengan indah. Ini bukan untuk mengatakan bahwa efek samping selalu jahat, tetapi mengendalikannya sangat berguna sehingga alasan ini saja sudah cukup untuk menggunakan bahasa malas.
sumber
Misalkan Anda memiliki banyak perhitungan mahal yang ditawarkan, tetapi tidak tahu mana yang benar-benar dibutuhkan, atau dalam urutan apa. Anda bisa menambahkan protokol ibu-may-i yang rumit untuk memaksa konsumen mencari tahu apa yang tersedia dan memicu perhitungan yang belum dilakukan. Atau Anda bisa menyediakan antarmuka yang bertindak seolah-olah perhitungan sudah dilakukan.
Juga, anggaplah Anda memiliki hasil yang tak terbatas. Himpunan semua bilangan prima misalnya. Sudah jelas bahwa Anda tidak dapat menghitung set sebelumnya, sehingga operasi apa pun dalam domain bilangan prima harus malas.
sumber
dengan evaluasi malas Anda tidak kehilangan kendali tentang eksekusi kode, itu masih mutlak deterministik. Sulit untuk membiasakan diri dengannya.
evaluasi malas berguna karena ini adalah cara pengurangan istilah lambda yang akan berakhir dalam beberapa kasus, di mana evaluasi yang bersemangat akan gagal, tetapi tidak sebaliknya. Ini termasuk 1) ketika Anda perlu menautkan ke hasil perhitungan sebelum Anda benar-benar menjalankan perhitungan, misalnya, ketika Anda membangun struktur grafik siklik, tetapi ingin melakukannya dengan gaya fungsional 2) ketika Anda menentukan struktur data yang tak terbatas, tetapi berfungsi umpan struktur ini untuk menggunakan hanya sebagian dari struktur data.
sumber