Mengapa konsep evaluasi malas bermanfaat?

30

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?

akashchandrakar
sumber
10
Dalam kebanyakan kasus itu tidak masalah. Untuk semua yang lain, Anda bisa menegakkan keketatan.
Cat Plus Plus
22
Inti dari bahasa murni fungsional seperti haskell adalah Anda tidak perlu repot ketika kode dijalankan, karena bebas efek samping.
bitmask
21
Anda harus berhenti berpikir tentang "mengeksekusi kode" dan mulai berpikir "menghitung hasil", karena itulah yang benar-benar Anda inginkan dalam masalah yang paling menarik. Tentu saja program biasanya juga perlu berinteraksi dengan lingkungan dalam beberapa cara, tetapi itu sering dapat dikurangi menjadi bagian kecil dari kode. Selebihnya, Anda dapat bekerja murni fungsional , dan kemalasan dapat membuat penalaran menjadi lebih sederhana.
leftaroundabout
6
Pertanyaan dalam judul ("Mengapa menggunakan evaluasi malas?") Sangat berbeda dari pertanyaan di dalam tubuh ("Bagaimana Anda menggunakan evaluasi malas?"). Untuk yang pertama, lihat jawaban saya untuk pertanyaan terkait ini .
Daniel Wagner
1
Contoh ketika kemalasan berguna: Dalam Haskell head . sortmemiliki O(n)kompleksitas karena kemalasan (tidak O(n log n)). Lihat Evaluasi Malas dan Kompleksitas Waktu .
Petr Pudlák

Jawaban:

62

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

[Evaluasi malas] membuatnya praktis untuk memodulasi program sebagai generator yang membangun sejumlah besar jawaban yang mungkin, dan pemilih yang memilih yang sesuai.

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:

  1. Menggabungkan generasi dan konsumsi ke dalam fungsi yang sama;
  2. Menulis produsen yang bekerja secara optimal hanya untuk konsumen tertentu;
  3. Menerapkan versi kemalasan Anda sendiri.
sakundim
sumber
Silakan info lebih lanjut atau contoh. Ini terdengar menarik.
Alex Nye
1
@AlexNye: Makalah John Hughes memiliki info lebih lanjut. Meskipun menjadi makalah akademis --- dan karena itu tidak diragukan lagi intimidasi --- itu sebenarnya sangat mudah diakses dan dibaca. Jika bukan karena panjangnya, mungkin akan cocok sebagai jawaban di sini!
Tikhon Jelvis
Mungkin untuk memahami jawaban ini, seseorang harus membaca koran karya Hughes ... tanpa melakukannya, saya masih gagal melihat bagaimana dan mengapa kemalasan dan modularitas terkait.
stakx
@stakx Tanpa deskripsi yang lebih baik, mereka sepertinya tidak terkait kecuali secara kebetulan. Keuntungan dari kemalasan dalam contoh ini adalah generator yang malas mampu menghasilkan semua keadaan yang mungkin dari permainan, tetapi tidak akan membuang waktu / memori melakukannya karena hanya yang terjadi yang akan dikonsumsi. Generator dapat dipisahkan dari konsumen tanpa menjadi generator malas, dan dimungkinkan (walaupun lebih sulit) menjadi malas tanpa dipisahkan dari konsumen.
Izkata
@Iztaka: "Generator dapat dipisahkan dari konsumen tanpa menjadi generator yang malas, dan dimungkinkan (walaupun lebih sulit) menjadi malas tanpa dipisahkan dari konsumen." Perhatikan bahwa ketika Anda menggunakan rute ini, Anda mungkin berakhir dengan ** generator khusus berlebihan ** - generator ditulis untuk mengoptimalkan satu konsumen, dan ketika digunakan kembali untuk orang lain itu tidak optimal. Contoh umum adalah pemetaan objek-relasional yang mengambil dan membangun grafik objek keseluruhan hanya karena Anda ingin satu string dari objek root. Kemalasan menghindari banyak kasus seperti itu (tetapi tidak semua).
sacundim
32

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?

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.

sepp2k
sumber
15
Inilah sebabnya mengapa hanya bahasa dengan "kemurnian yang dipaksakan" seperti Haskell yang mendukung kemalasan di mana saja secara default. Bahasa "yang didorong kemurnian" seperti Scala membutuhkan programmer untuk secara eksplisit mengatakan di mana mereka ingin kemalasan, dan terserah kepada programmer untuk memastikan bahwa kemalasan tidak akan menimbulkan masalah. Bahasa yang memiliki kemalasan secara default dan memiliki efek samping yang tidak terlacak memang akan sulit diprogram.
Ben
1
pasti monad selain IO juga dapat menyebabkan efek samping
jk.
1
@jk Hanya IO yang dapat menyebabkan efek samping eksternal .
dave4420
@ dave4420 ya tapi bukan itu yang dikatakan jawaban ini
jk.
1
@ jk Di Haskell sebenarnya tidak. Tidak ada monad kecuali untuk IO (atau yang dibangun di atas IO) memiliki efek samping. Dan ini hanya karena kompiler memperlakukan IO secara berbeda. Itu menganggap IO sebagai "Immutable Off". Monads hanyalah cara (pintar) untuk memastikan urutan eksekusi tertentu (jadi file Anda hanya akan dihapus setelah pengguna memasukkan "ya").
scarfridge
22

Jika Anda terbiasa dengan database, cara yang sangat sering untuk memproses data adalah:

  • Ajukan pertanyaan seperti select * from foobar
  • Meskipun ada lebih banyak data, lakukan: dapatkan baris hasil berikutnya dan proseskan

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)

samthebrand
sumber
Poin bagus. Misalnya Collection2.filter()(serta metode lain dari kelas itu) cukup banyak menerapkan evaluasi malas: hasilnya "terlihat seperti" biasa Collection, tetapi urutan pelaksanaannya mungkin tidak intuitif (atau setidaknya tidak jelas). Juga, ada yielddalam Python (dan fitur serupa di C #, yang saya tidak ingat nama) yang bahkan lebih dekat untuk mendukung evaluasi malas daripada Iterator normal.
Joachim Sauer
@ JoachimSauer dalam C # hasil pengembaliannya, atau tentu saja Anda dapat menggunakan oprerators linq prebuild, sekitar setengahnya adalah malas
jk.
+1: Untuk menyebutkan iterator dalam bahasa imperatif / berorientasi objek. Saya menggunakan solusi serupa untuk mengimplementasikan fungsi stream dan stream di Java. Menggunakan iterator saya bisa memiliki fungsi seperti take (n), dropWhile () pada input stream yang panjangnya tidak diketahui.
Giorgio
13

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.

You                                            Program Processor
------------------------------------------------------------------------------
Get the first 2000 users ---------->---------- OK!
                         --------------------- So I'll go get those records...
WAIT! Also, they have to ---------->---------- Gotcha!
start with "Ab"
                         --------------------- NOW I'll get them...
WAIT! Make sure they're  ---------->---------- Good idea Boss!
over 20!
                         --------------------- Let's go then...
And one more thing! Make ---------->---------- Anything else? Ugh!
sure they're male!

No that is all. :(       ---------->---------- FINE! Getting records!

                         --------------------- Here you go. 
Thanks Postgres, you're  ---------->----------  ...
my only friend.

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.

sergserg
sumber
1
ini adalah IMHO penjelasan yang sangat buruk. Sayangnya saya tidak memiliki cukup perwakilan di situs SE khusus ini untuk memilihnya. Poin sebenarnya dari evaluasi malas adalah bahwa tidak ada hasil ini yang benar-benar dihasilkan sampai ada yang lain siap untuk mengkonsumsinya.
Alnitak
Jawaban saya yang diposting mengatakan hal yang persis sama dengan komentar Anda.
Sergserg
Itu adalah Programor Program yang sangat sopan.
Julian
9

Evaluasi ekspresi yang malas akan menyebabkan perancang kode yang diberikan kehilangan kontrol atas urutan kode mereka dieksekusi.

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.

Caleb
sumber
5

Ada beberapa argumen untuk evaluasi malas yang menurut saya menarik

  1. 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

    take 10 . filter (<1) . map (1/)
    

    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 praktik

  2. Lebih 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.

  3. 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.

  4. 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.

Philip JF
sumber
2

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.

ddyer
sumber
1

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.

permeakra
sumber