ฉันได้ยินมาโดยทั่วไปว่ารหัสการผลิตควรหลีกเลี่ยงการใช้ Lazy I / O คำถามของฉันคือทำไม? เป็นเรื่องปกติหรือไม่ที่จะใช้ Lazy I / O นอกเหนือจากการเล่นเฉยๆ? และอะไรทำให้ทางเลือกอื่น (เช่นตัวนับ) ดีกว่า?
ฉันได้ยินมาโดยทั่วไปว่ารหัสการผลิตควรหลีกเลี่ยงการใช้ Lazy I / O คำถามของฉันคือทำไม? เป็นเรื่องปกติหรือไม่ที่จะใช้ Lazy I / O นอกเหนือจากการเล่นเฉยๆ? และอะไรทำให้ทางเลือกอื่น (เช่นตัวนับ) ดีกว่า?
คำตอบ:
Lazy IO มีปัญหาว่าการปล่อยทรัพยากรใดก็ตามที่คุณได้มานั้นค่อนข้างไม่สามารถคาดเดาได้เนื่องจากขึ้นอยู่กับว่าโปรแกรมของคุณใช้ข้อมูลอย่างไรนั่นคือ "รูปแบบความต้องการ" เมื่อโปรแกรมของคุณลดการอ้างอิงสุดท้ายไปยังทรัพยากร GC จะทำงานและปล่อยทรัพยากรนั้นในที่สุด
เลซี่สตรีมเป็นรูปแบบที่สะดวกมากในการตั้งโปรแกรมด้วยเหตุนี้ท่อหอยจึงสนุกและเป็นที่นิยม
อย่างไรก็ตามหากทรัพยากรมีข้อ จำกัด (เช่นในสถานการณ์ที่มีประสิทธิภาพสูงหรือสภาพแวดล้อมการผลิตที่คาดว่าจะปรับขนาดจนถึงขีด จำกัด ของเครื่อง) โดยอาศัย GC ในการล้างข้อมูลอาจเป็นการรับประกันไม่เพียงพอ
บางครั้งคุณต้องปล่อยทรัพยากรอย่างกระตือรือร้นเพื่อปรับปรุงความสามารถในการปรับขนาด
แล้วทางเลือกอื่นสำหรับ IO ที่ขี้เกียจซึ่งไม่ได้หมายความว่าจะยอมแพ้กับการประมวลผลแบบเพิ่มหน่วย (ซึ่งจะใช้ทรัพยากรมากเกินไป)? เรามีfoldl
พื้นฐานการประมวลผลหรือที่รู้จักกันในชื่อวนซ้ำหรือตัวนับซึ่งแนะนำโดยโอเล็กคิลิซอฟในช่วงปลายยุค 2000และนับตั้งแต่ได้รับความนิยมจากโครงการที่ใช้ระบบเครือข่ายจำนวนมาก
แทนที่จะประมวลผลข้อมูลเป็นสตรีมที่เกียจคร้านหรือเป็นกลุ่มใหญ่ ๆ เราแทนที่จะใช้การประมวลผลที่เข้มงวดแบบเป็นกลุ่มโดยรับประกันการสรุปทรัพยากรเมื่ออ่านข้อมูลครั้งสุดท้ายแล้ว นั่นคือสาระสำคัญของการเขียนโปรแกรมแบบวนซ้ำและข้อ จำกัด ด้านทรัพยากรที่ดีมาก
ข้อเสียของ IO ที่ใช้ iteratee คือมันมีรูปแบบการเขียนโปรแกรมที่ค่อนข้างอึดอัด (คล้ายกับการเขียนโปรแกรมตามเหตุการณ์เมื่อเทียบกับการควบคุมแบบเธรดที่ดี) เป็นเทคนิคขั้นสูงในภาษาโปรแกรมใด ๆ และสำหรับปัญหาการเขียนโปรแกรมส่วนใหญ่ขี้เกียจ IO เป็นที่น่าพอใจ อย่างไรก็ตามหากคุณจะเปิดไฟล์จำนวนมากหรือพูดคุยกับซ็อกเก็ตจำนวนมากหรือใช้ทรัพยากรพร้อมกันจำนวนมากวิธีการวนซ้ำ (หรือตัวนับ) อาจเหมาะสม
Dons ให้คำตอบที่ดีมาก แต่เขาไม่ได้ทิ้งสิ่งที่เป็น (สำหรับฉัน) หนึ่งในคุณสมบัติที่น่าสนใจที่สุดของการวนซ้ำ: ทำให้ง่ายต่อการให้เหตุผลเกี่ยวกับการจัดการพื้นที่เนื่องจากข้อมูลเก่าจะต้องถูกเก็บไว้อย่างชัดเจน พิจารณา:
average :: [Float] -> Float
average xs = sum xs / length xs
นี้เป็นที่รู้จักกันดีการรั่วไหลของพื้นที่เพราะรายชื่อทั้งหมดxs
จะต้องถูกเก็บไว้ในหน่วยความจำในการคำนวณทั้งสองและsum
length
เป็นไปได้ที่จะสร้างผู้บริโภคที่มีประสิทธิภาพโดยสร้างพับ:
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'
แต่ค่อนข้างไม่สะดวกที่จะต้องทำสิ่งนี้กับโปรเซสเซอร์สตรีมทุกตัว มีการสรุปบางอย่าง ( Conal Elliott - Beautiful Fold Zipping ) แต่ดูเหมือนจะไม่ติด อย่างไรก็ตามการวนซ้ำจะช่วยให้คุณมีระดับการแสดงออกที่ใกล้เคียงกัน
aveIter = uncurry (/) <$> I.zip I.sum I.length
สิ่งนี้ไม่มีประสิทธิภาพเท่ากับการพับเนื่องจากรายการยังคงวนซ้ำหลาย ๆ ครั้งอย่างไรก็ตามจะรวบรวมเป็นกลุ่มเพื่อให้สามารถรวบรวมข้อมูลเก่าได้อย่างมีประสิทธิภาพ ในการทำลายคุณสมบัตินั้นจำเป็นต้องเก็บอินพุตทั้งหมดไว้อย่างชัดเจนเช่นด้วย stream2list:
badAveIter = (\xs -> sum xs / length xs) <$> I.stream2list
สถานะของการวนซ้ำในรูปแบบการเขียนโปรแกรมเป็นงานที่อยู่ระหว่างดำเนินการอย่างไรก็ตามมันดีกว่าปีที่แล้วมาก เรากำลังเรียนรู้สิ่งที่ combinators มีประโยชน์ (เช่นzip
, breakE
,enumWith
) และที่น้อยดังนั้นมีผลว่าในตัว iteratees และ combinators ให้ expressivity อย่างต่อเนื่องมากขึ้น
ที่กล่าวว่าดอนส์ถูกต้องว่าเป็นเทคนิคขั้นสูง แน่นอนฉันจะไม่ใช้มันสำหรับทุกปัญหา I / O
ฉันใช้ I / O ที่ขี้เกียจในรหัสการผลิตตลอดเวลา เป็นเพียงปัญหาในบางสถานการณ์เช่นที่ดอนกล่าวถึง แต่สำหรับการอ่านเพียงไม่กี่ไฟล์ก็ใช้ได้ดี
อัปเดต:เมื่อเร็ว ๆ นี้ใน haskell-cafe Oleg Kiseljov แสดงให้เห็นว่าunsafeInterleaveST
(ซึ่งใช้สำหรับการใช้งาน IO ที่ขี้เกียจภายใน ST monad) นั้นไม่ปลอดภัยมาก - มันทำลายเหตุผลที่เท่าเทียมกัน เขาแสดงให้เห็นว่าอนุญาตให้สร้างbad_ctx :: ((Bool,Bool) -> Bool) -> Bool
เช่นนั้นได้
> bad_ctx (\(x,y) -> x == y)
True
> bad_ctx (\(x,y) -> y == x)
False
แม้ว่าจะ==
เป็นการสับเปลี่ยน
ปัญหาอื่นเกี่ยวกับ IO ที่ขี้เกียจ: การดำเนินการ IO จริงอาจถูกเลื่อนออกไปจนกว่าจะสายเกินไปเช่นหลังจากปิดไฟล์แล้ว อ้างจากHaskell Wiki - ปัญหาเกี่ยวกับ IO ที่ขี้เกียจ :
ตัวอย่างเช่นข้อผิดพลาดทั่วไปของผู้เริ่มต้นคือการปิดไฟล์ก่อนที่จะอ่านเสร็จ:
wrong = do fileData <- withFile "test.txt" ReadMode hGetContents putStr fileData
ปัญหาคือด้วย File ปิดจุดจับก่อนที่ fileData จะถูกบังคับ วิธีที่ถูกต้องคือส่งรหัสทั้งหมดไปที่ withFile:
right = withFile "test.txt" ReadMode $ \handle -> do fileData <- hGetContents handle putStr fileData
ที่นี่ข้อมูลจะถูกใช้ก่อนที่ไฟล์จะเสร็จสิ้น
ซึ่งมักจะเกิดขึ้นโดยไม่คาดคิดและเป็นข้อผิดพลาดที่ง่าย
ดูเพิ่มเติม: สามตัวอย่างของปัญหาเกี่ยวกับการขี้เกียจ I / O
hGetContents
และwithFile
ไม่มีจุดหมายเพราะในอดีตทำให้แฮนเดิลอยู่ในสถานะ "ปิดหลอก" และจะจัดการปิดให้คุณ (อย่างเกียจคร้าน) ดังนั้นรหัสจึงเทียบเท่ากันทุกประการreadFile
หรือแม้กระทั่งopenFile
ไม่มี hClose
นั่นเป็นสิ่งที่ขี้เกียจ I / O มี หากคุณไม่ได้ใช้readFile
, getContents
หรือhGetContents
คุณไม่ได้ใช้ขี้เกียจ I / O ตัวอย่างเช่นใช้line <- withFile "test.txt" ReadMode hGetLine
งานได้ดี
hGetContents
จะจัดการปิดไฟล์ให้คุณ แต่ก็ยังอนุญาตให้ปิดได้ด้วยตัวเอง "ก่อนกำหนด" และช่วยให้มั่นใจได้ว่าทรัพยากรจะถูกปล่อยออกมาอย่างคาดเดาได้
อีกปัญหาหนึ่งของ IO ที่ขี้เกียจที่ยังไม่ได้กล่าวถึงก็คือมันมีพฤติกรรมที่น่าแปลกใจ ในโปรแกรม Haskell ปกติบางครั้งอาจเป็นเรื่องยากที่จะคาดเดาเมื่อแต่ละส่วนของโปรแกรมของคุณได้รับการประเมิน แต่โชคดีเนื่องจากความบริสุทธิ์จึงไม่สำคัญเว้นแต่คุณจะมีปัญหาด้านประสิทธิภาพ เมื่อมีการนำ IO ขี้เกียจมาใช้ลำดับการประเมินของโค้ดของคุณมีผลต่อความหมายดังนั้นการเปลี่ยนแปลงที่คุณเคยคิดว่าไม่เป็นอันตรายอาจทำให้คุณเกิดปัญหาได้
ตัวอย่างเช่นนี่คือคำถามเกี่ยวกับโค้ดที่ดูสมเหตุสมผล แต่ทำให้สับสนมากขึ้นโดย IO ที่รอการตัดบัญชี: withFile กับ openFile
ปัญหาเหล่านี้ไม่ได้ร้ายแรงเสมอไป แต่เป็นอีกสิ่งหนึ่งที่ต้องนึกถึงและอาการปวดหัวที่รุนแรงพอสมควรที่ฉันเองจะหลีกเลี่ยง IO ที่ขี้เกียจเว้นแต่จะมีปัญหาจริงในการทำงานทั้งหมดล่วงหน้า