Có gì xấu về I / O Lười biếng?


89

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:


81

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ó foldlxử 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.


22
Vì tôi vừa theo liên kết đến câu hỏi cũ này từ một cuộc thảo luận về I / O lười biếng, tôi nghĩ rằng tôi sẽ thêm một lưu ý rằng kể từ đó, phần lớn sự khó xử của các lần lặp đã được thay thế bởi các thư viện phát trực tuyến mới như pipeconduit .
Ørjan Johansen vào

40

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 xsphải được giữ lại trong bộ nhớ để tính toán cả sumlength. 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.


25

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.


Tôi cũng sử dụng I / O lười biếng. Tôi chuyển sang lặp lại khi muốn kiểm soát nhiều hơn việc quản lý tài nguyên.
John L

20

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

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


Trên thực tế, việc kết hợp hGetContentswithFilevô 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 readFilehoặc thậm chí openFilekhông có hClose. Đó là cơ bản những gì lười biếng I / O . Nếu bạn không sử dụng readFile, getContentshoặc hGetContentsbạn không sử dụng lười biếng I / O. Ví dụ line <- withFile "test.txt" ReadMode hGetLinehoạt động tốt.
Dag

1
@Dag: mặc dù hGetContentssẽ 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.
Ben Millwood

17

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.

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.