Apa buruknya Lazy I / O?

89

Saya biasanya mendengar bahwa kode produksi harus menghindari penggunaan Lazy I / O. Pertanyaan saya adalah, mengapa? Apakah saya boleh menggunakan Lazy I / O selain hanya bermain-main? Dan apa yang membuat alternatif (misalnya pencacah) lebih baik?

Dan Burton
sumber

Jawaban:

81

Lazy IO memiliki masalah bahwa melepaskan sumber daya apa pun yang Anda peroleh agak tidak dapat diprediksi, karena bergantung pada bagaimana program Anda menggunakan data - "pola permintaan" -nya. Setelah program Anda melepaskan referensi terakhir ke sumber daya, GC pada akhirnya akan berjalan dan melepaskan sumber daya itu.

Aliran malas adalah gaya yang sangat nyaman untuk diprogram. Inilah sebabnya pipa shell sangat menyenangkan dan populer.

Namun, jika sumber daya dibatasi (seperti dalam skenario kinerja tinggi, atau lingkungan produksi yang berharap untuk mencapai batas alat berat), mengandalkan GC untuk pembersihan dapat menjadi jaminan yang tidak memadai.

Terkadang Anda harus melepaskan sumber daya dengan bersemangat, untuk meningkatkan skalabilitas.

Jadi apa alternatif untuk IO malas yang tidak berarti menyerah pada pemrosesan tambahan (yang pada gilirannya akan menghabiskan terlalu banyak sumber daya)? Nah, kami memiliki foldlpemrosesan berbasis, alias iteratees atau enumerator, yang diperkenalkan oleh Oleg K tepatnyaov pada akhir 2000-an , dan sejak itu dipopulerkan oleh sejumlah proyek berbasis jaringan.

Alih-alih memproses data sebagai aliran lambat, atau dalam satu batch besar, kami malah mengabstraksi pemrosesan ketat berbasis chunk, dengan jaminan finalisasi sumber daya setelah potongan terakhir dibaca. Itulah inti dari pemrograman berbasis iteratee, dan yang menawarkan batasan sumber daya yang sangat bagus.

Kelemahan dari IO berbasis iteratee adalah ia memiliki model pemrograman yang agak canggung (kira-kira analog dengan pemrograman berbasis acara, versus kontrol berbasis thread yang bagus). Ini jelas merupakan teknik tingkat lanjut, dalam bahasa pemrograman apa pun. Dan untuk sebagian besar masalah pemrograman, lazy IO sepenuhnya memuaskan. Namun, jika Anda akan membuka banyak file, atau berbicara di banyak soket, atau menggunakan banyak sumber daya secara bersamaan, pendekatan iteratee (atau enumerator) mungkin masuk akal.

Don Stewart
sumber
22
Karena saya baru saja mengikuti tautan ke pertanyaan lama ini dari diskusi tentang I / O yang malas, saya pikir saya akan menambahkan catatan bahwa sejak saat itu, sebagian besar kecanggungan dari iterasi telah digantikan oleh perpustakaan streaming baru seperti pipa dan saluran .
Ørjan Johansen
40

Dons telah memberikan jawaban yang sangat bagus, tetapi dia tidak memberikan apa yang (bagi saya) salah satu fitur yang paling menarik dari iterasi: fitur ini mempermudah untuk bernalar tentang manajemen ruang karena data lama harus disimpan secara eksplisit. Mempertimbangkan:

average :: [Float] -> Float
average xs = sum xs / length xs

Ini adalah kebocoran ruang yang terkenal, karena seluruh daftar xsharus disimpan dalam memori untuk menghitung sumdan length. Anda dapat membuat konsumen yang efisien dengan membuat lipatan:

average2 :: [Float] -> Float
average2 xs = uncurry (/) <$> foldl (\(sumT, n) x -> (sumT+x, n+1)) (0,0) xs
-- N.B. this will build up thunks as written, use a strict pair and foldl'

Tetapi agak merepotkan untuk melakukan ini untuk setiap prosesor streaming. Ada beberapa generalisasi ( Conal Elliott - Beautiful Fold Zipping ), tetapi tampaknya tidak berhasil. Namun, iterasi bisa memberi Anda tingkat ekspresi yang serupa.

aveIter = uncurry (/) <$> I.zip I.sum I.length

Ini tidak seefisien flip karena daftarnya masih diulang beberapa kali, namun dikumpulkan dalam potongan sehingga data lama dapat dikumpulkan sampah secara efisien. Untuk merusak properti itu, penting untuk secara eksplisit mempertahankan seluruh input, seperti dengan stream2list:

badAveIter = (\xs -> sum xs / length xs) <$> I.stream2list

Keadaan iterasi sebagai model pemrograman sedang dalam proses, namun jauh lebih baik daripada setahun yang lalu. Kami belajar sedang combinators apa yang berguna (misalnya zip, breakE, enumWith) dan yang kurang begitu, dengan hasil yang built-in iteratees dan combinators memberikan terus lebih ekspresivitas.

Yang mengatakan, Don benar bahwa mereka adalah teknik tingkat lanjut; Saya pasti tidak akan menggunakannya untuk setiap masalah I / O.

John L.
sumber
25

Saya menggunakan lazy I / O dalam kode produksi sepanjang waktu. Ini hanya masalah dalam keadaan tertentu, seperti yang disebutkan Don. Tetapi untuk hanya membaca beberapa file, ini berfungsi dengan baik.

agustus
sumber
Saya juga menggunakan I / O malas. Saya beralih ke iterasi ketika saya ingin lebih mengontrol manajemen sumber daya.
John L
20

Pembaruan: Baru-baru ini di haskell-cafe Oleg Kiseljov menunjukkan bahwa unsafeInterleaveST(yang digunakan untuk mengimplementasikan IO malas dalam monad ST) sangat tidak aman - itu merusak penalaran persamaan. Dia menunjukkan bahwa itu memungkinkan untuk membangun bad_ctx :: ((Bool,Bool) -> Bool) -> Bool seperti itu

> bad_ctx (\(x,y) -> x == y)
True
> bad_ctx (\(x,y) -> y == x)
False

meski ==bersifat komutatif.


Masalah lain dengan lazy IO: Operasi IO yang sebenarnya dapat ditunda hingga terlambat, misalnya setelah file ditutup. Mengutip dari Haskell Wiki - Masalah dengan IO yang malas :

Misalnya, kesalahan umum pemula adalah menutup file sebelum selesai membacanya:

wrong = do
    fileData <- withFile "test.txt" ReadMode hGetContents
    putStr fileData

Masalahnya adalah withFile menutup pegangan sebelum fileData dipaksa. Cara yang benar adalah dengan meneruskan semua kode ke withFile:

right = withFile "test.txt" ReadMode $ \handle -> do
    fileData <- hGetContents handle
    putStr fileData

Di sini, data digunakan sebelum file selesai.

Ini sering tidak terduga dan merupakan kesalahan yang mudah dilakukan.


Lihat juga: Tiga contoh masalah dengan malas I / O .

Petr
sumber
Sebenarnya menggabungkan hGetContentsdan withFiletidak ada gunanya karena yang pertama menempatkan pegangan dalam keadaan "pseudo-closed" dan akan menangani penutupan untuk Anda (malas) sehingga kodenya persis sama readFile, atau bahkan openFiletanpa hClose. Itu pada dasarnya apa malas I / O adalah . Jika Anda tidak menggunakan readFile, getContentsatau hGetContentsAnda tidak menggunakan I / O yang malas. Misalnya line <- withFile "test.txt" ReadMode hGetLineberfungsi dengan baik.
Dag
1
@Dag: meskipun hGetContentsakan menangani penutupan file untuk Anda, itu juga diizinkan untuk menutupnya sendiri "lebih awal", dan membantu memastikan sumber daya dirilis secara dapat diprediksi.
Ben Millwood
17

Masalah lain dengan lazy IO yang belum disebutkan sejauh ini adalah perilaku yang mengejutkan. Dalam program Haskell normal, terkadang sulit untuk memprediksi kapan setiap bagian dari program Anda dievaluasi, tetapi untungnya karena kemurniannya tidak masalah kecuali Anda memiliki masalah kinerja. Saat lazy IO diperkenalkan, urutan evaluasi kode Anda benar-benar berpengaruh pada artinya, sehingga perubahan yang biasa Anda anggap tidak berbahaya dapat menyebabkan masalah nyata bagi Anda.

Sebagai contoh, berikut adalah pertanyaan tentang kode yang terlihat masuk akal tetapi dibuat lebih membingungkan oleh IO yang ditangguhkan: withFile vs. openFile

Masalah-masalah ini tidak selalu fatal, tetapi ini adalah hal lain yang perlu dipikirkan, dan sakit kepala yang cukup parah sehingga saya pribadi menghindari IO yang malas kecuali ada masalah nyata dengan melakukan semua pekerjaan di muka.

Ben Millwood
sumber