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?
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?
Jawaban:
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 foldl
pemrosesan 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.
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 xs
harus disimpan dalam memori untuk menghitung sum
dan 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.
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.
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 .
hGetContents
dan withFile
tidak 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 openFile
tanpa hClose
. Itu pada dasarnya apa malas I / O adalah . Jika Anda tidak menggunakan readFile
, getContents
atau hGetContents
Anda tidak menggunakan I / O yang malas. Misalnya line <- withFile "test.txt" ReadMode hGetLine
berfungsi dengan baik.
hGetContents
akan menangani penutupan file untuk Anda, itu juga diizinkan untuk menutupnya sendiri "lebih awal", dan membantu memastikan sumber daya dirilis secara dapat diprediksi.
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.