Tôi thường nghe nói rằng mã sản xuất nên tránh sử dụng Lazy I / O. Câu hỏi của tôi là, tại sao? Có bao giờ sử dụng Lazy I / O ngoài việc đùa giỡn không? Và điều gì làm cho các lựa chọn thay thế (ví dụ: điều tra viên) tốt hơn?
Tôi thường nghe nói rằng mã sản xuất nên tránh sử dụng Lazy I / O. Câu hỏi của tôi là, tại sao? Có bao giờ sử dụng Lazy I / O ngoài việc đùa giỡn không? Và điều gì làm cho các lựa chọn thay thế (ví dụ: điều tra viên) tốt hơn?
Câu trả lời:
Lazy IO có vấn đề là việc giải phóng bất kỳ tài nguyên nào bạn đã có được là hơi khó đoán, vì nó phụ thuộc vào cách chương trình của bạn sử dụng dữ liệu - "mẫu nhu cầu" của nó. Khi chương trình của bạn bỏ tham chiếu cuối cùng đến tài nguyên, GC cuối cùng sẽ chạy và giải phóng tài nguyên đó.
Luzy stream là một phong cách rất thuận tiện để lập trình. Đây là lý do tại sao shell pipe lại rất thú vị và phổ biến.
Tuy nhiên, nếu nguồn lực bị hạn chế (như trong các tình huống hiệu suất cao hoặc môi trường sản xuất mong đợi mở rộng đến giới hạn của máy) thì việc dựa vào GC để dọn dẹp có thể là một sự đảm bảo không đủ.
Đôi khi bạn phải háo hức giải phóng tài nguyên để cải thiện khả năng mở rộng.
Vậy đâu là lựa chọn thay thế cho IO lười biếng không có nghĩa là từ bỏ quá trình xử lý gia tăng (do đó sẽ tiêu tốn quá nhiều tài nguyên)? Chà, chúng tôi có foldl
xử lý dựa trên, hay còn gọi là lặp lại hoặc điều tra, được giới thiệu bởi Oleg Kiselyov vào cuối những năm 2000 và kể từ đó được phổ biến bởi một số dự án dựa trên mạng.
Thay vì xử lý dữ liệu dưới dạng luồng lười hoặc trong một lô lớn, thay vào đó, chúng tôi trừu tượng hóa qua xử lý nghiêm ngặt dựa trên phân đoạn, với đảm bảo hoàn thành tài nguyên sau khi phân đoạn cuối cùng được đọc. Đó là bản chất của lập trình dựa trên lặp đi lặp lại, và một trong đó cung cấp các ràng buộc tài nguyên rất tốt.
Nhược điểm của IO dựa trên lặp lại là nó có một mô hình lập trình hơi khó xử (gần giống với lập trình dựa trên sự kiện, so với điều khiển dựa trên luồng đẹp). Nó chắc chắn là một kỹ thuật tiên tiến, trong bất kỳ ngôn ngữ lập trình nào. Và đối với phần lớn các vấn đề lập trình, IO lười biếng là hoàn toàn thỏa đáng. Tuy nhiên, nếu bạn sẽ mở nhiều tệp, hoặc nói chuyện trên nhiều ổ cắm, hoặc sử dụng nhiều tài nguyên đồng thời, cách tiếp cận lặp lại (hoặc người điều tra) có thể có ý nghĩa.
Dons đã đưa ra một câu trả lời rất hay, nhưng anh ấy đã bỏ qua (đối với tôi) một trong những tính năng hấp dẫn nhất của các lần lặp: chúng giúp lý luận về quản lý không gian dễ dàng hơn vì dữ liệu cũ phải được giữ lại một cách rõ ràng. Xem xét:
average :: [Float] -> Float
average xs = sum xs / length xs
Đây là một rò rỉ không gian nổi tiếng, vì toàn bộ danh sách xs
phải được giữ lại trong bộ nhớ để tính toán cả sum
và length
. Có thể tạo ra một người tiêu dùng hiệu quả bằng cách tạo ra một trang:
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'
Nhưng hơi bất tiện khi phải làm điều này cho mọi bộ xử lý luồng. Có một số khái quát ( Conal Elliott - Beautiful Fold Zipping ), nhưng chúng dường như chưa bắt kịp. Tuy nhiên, các lần lặp lại có thể giúp bạn có một mức biểu thức tương tự.
aveIter = uncurry (/) <$> I.zip I.sum I.length
Điều này không hiệu quả bằng màn hình đầu tiên vì danh sách vẫn được lặp đi lặp lại nhiều lần, tuy nhiên nó được thu thập theo nhiều phần để dữ liệu cũ có thể được thu thập rác một cách hiệu quả. Để phá vỡ thuộc tính đó, cần giữ lại toàn bộ dữ liệu đầu vào một cách rõ ràng, chẳng hạn như với stream2list:
badAveIter = (\xs -> sum xs / length xs) <$> I.stream2list
Trạng thái lặp lại như một mô hình lập trình đang được tiến hành, tuy nhiên nó đã tốt hơn nhiều so với cách đây một năm. Chúng tôi đang học gì combinators là hữu ích (ví dụ zip
, breakE
, enumWith
) và được cho là kém như vậy, với kết quả là built-in iteratees và combinators cung cấp expressivity liên tục hơn.
Điều đó nói rằng, Dons chính xác rằng chúng là một kỹ thuật tiên tiến; Tôi chắc chắn sẽ không sử dụng chúng cho mọi vấn đề I / O.
Tôi sử dụng I / O lười biếng trong mã sản xuất mọi lúc. Nó chỉ là một vấn đề trong một số trường hợp nhất định, như Don đã đề cập. Nhưng chỉ cần đọc một vài tệp, nó hoạt động tốt.
Cập nhật: Gần đây trên haskell-cafe, Oleg Kiseljov đã chỉ ra rằng unsafeInterleaveST
(được sử dụng để triển khai IO lười biếng trong đơn nguyên ST) là rất không an toàn - nó phá vỡ lý luận cân bằng. Ông chỉ ra rằng nó cho phép để xây dựng bad_ctx :: ((Bool,Bool) -> Bool) -> Bool
mà
> bad_ctx (\(x,y) -> x == y)
True
> bad_ctx (\(x,y) -> y == x)
False
mặc dù ==
là giao hoán.
Một vấn đề khác với IO lười biếng: Hoạt động IO thực tế có thể bị hoãn lại cho đến khi quá muộn, chẳng hạn như sau khi tệp được đóng. Trích dẫn từ Haskell Wiki - Các vấn đề với IO lười biếng :
Ví dụ: một lỗi phổ biến dành cho người mới bắt đầu là đóng tệp trước khi đọc xong:
wrong = do fileData <- withFile "test.txt" ReadMode hGetContents putStr fileData
Vấn đề là withFile đóng xử lý trước khi fileData bị buộc. Cách đúng là chuyển tất cả mã vào withFile:
right = withFile "test.txt" ReadMode $ \handle -> do fileData <- hGetContents handle putStr fileData
Tại đây, dữ liệu được sử dụng trước khi withFile kết thúc.
Điều này thường không mong muốn và là một lỗi dễ mắc phải.
Xem thêm: Ba ví dụ về vấn đề với I / O Lười biếng .
hGetContents
và withFile
vô nghĩa bởi vì cái trước đặt tay cầm ở trạng thái "giả đóng" và sẽ xử lý việc đóng cho bạn (một cách lười biếng) nên mã chính xác tương đương readFile
hoặc thậm chí openFile
không có hClose
. Đó là cơ bản những gì lười biếng I / O là . Nếu bạn không sử dụng readFile
, getContents
hoặc hGetContents
bạn không sử dụng lười biếng I / O. Ví dụ line <- withFile "test.txt" ReadMode hGetLine
hoạt động tốt.
hGetContents
sẽ xử lý việc đóng tệp cho bạn, nhưng bạn cũng có thể tự đóng tệp "sớm" và giúp đảm bảo tài nguyên được phát hành một cách dự đoán.
Một vấn đề khác với IO lười biếng chưa được đề cập cho đến nay là nó có hành vi đáng ngạc nhiên. Trong một chương trình Haskell thông thường, đôi khi có thể khó dự đoán khi từng phần của chương trình của bạn được đánh giá, nhưng may mắn thay, do độ tinh khiết nên nó thực sự không quan trọng trừ khi bạn gặp vấn đề về hiệu suất. Khi IO lười biếng được giới thiệu, thứ tự đánh giá mã của bạn thực sự có ảnh hưởng đến ý nghĩa của nó, vì vậy những thay đổi mà bạn quen coi là vô hại có thể gây ra cho bạn vấn đề thực sự.
Ví dụ, đây là một câu hỏi về mã có vẻ hợp lý nhưng lại khiến IO trì hoãn trở nên khó hiểu hơn: withFile so với openFile
Những vấn đề này không phải lúc nào cũng gây tử vong, nhưng đó là một điều khác cần phải suy nghĩ và một cơn đau đầu đủ nghiêm trọng mà cá nhân tôi tránh IO lười biếng trừ khi có vấn đề thực sự với việc thực hiện tất cả công việc từ trước.