Co jest takiego złego w Lazy I / O?


89

Generalnie słyszałem, że kod produkcyjny powinien unikać używania Lazy I / O. Moje pytanie brzmi: dlaczego? Czy używanie Lazy I / O poza zwykłą zabawą jest w porządku? A co sprawia, że ​​alternatywy (np. Rachmistrzowie) są lepsze?

Odpowiedzi:


81

Lazy IO ma problem z tym, że zwolnienie dowolnego zdobytego zasobu jest nieco nieprzewidywalne, ponieważ zależy to od tego, jak program zużywa dane - jego „wzorca zapotrzebowania”. Gdy program usunie ostatnie odwołanie do zasobu, GC ostatecznie uruchomi i zwolni ten zasób.

Leniwe strumienie są bardzo wygodnym stylem do programowania. Dlatego rury powłokowe są tak zabawne i popularne.

Jeśli jednak zasoby są ograniczone (jak w scenariuszach o wysokiej wydajności lub środowiskach produkcyjnych, które spodziewają się skalowania do granic maszyny) poleganie na GC do czyszczenia może być niewystarczającą gwarancją.

Czasami trzeba chętnie zwolnić zasoby, aby poprawić skalowalność.

Jakie są więc alternatywy dla leniwego IO, które nie oznaczają rezygnacji z przetwarzania przyrostowego (które z kolei pochłaniałoby zbyt wiele zasobów)? Cóż, mamy foldloparte na przetwarzaniu, czyli iteraty lub wyliczacze, wprowadzone przez Olega Kiselyova pod koniec 2000 roku , a od tego czasu spopularyzowane przez wiele projektów opartych na sieci.

Zamiast przetwarzać dane jako leniwe strumienie lub w jednej ogromnej partii, zamiast tego abstrakcyjne jest przetwarzanie ścisłe oparte na fragmentach, z gwarantowaną finalizacją zasobu po odczytaniu ostatniej porcji. To jest istota programowania opartego na iteracji, która oferuje bardzo ładne ograniczenia zasobów.

Wadą IO opartego na iteracji jest to, że ma nieco niezręczny model programowania (z grubsza analogiczny do programowania opartego na zdarzeniach, w porównaniu z przyjemną kontrolą opartą na wątkach). Jest to zdecydowanie zaawansowana technika w każdym języku programowania. W przypadku większości problemów programistycznych leniwe IO jest całkowicie satysfakcjonujące. Jednakże, jeśli będziesz otwierać wiele plików, rozmawiać na wielu gniazdach lub w inny sposób używać wielu jednoczesnych zasobów, podejście iteracyjne (lub wyliczające) może mieć sens.


22
Ponieważ właśnie podążałem za linkiem do tego starego pytania z dyskusji na temat leniwego wejścia / wyjścia, pomyślałem, że dodam uwagę, że od tego czasu wiele niezręczności iteratów zostało zastąpionych przez nowe biblioteki strumieniowe, takie jak rury i przewody .
Ørjan Johansen

40

Dons udzielił bardzo dobrej odpowiedzi, ale pominął to, co jest (dla mnie) jedną z najbardziej przekonujących cech iteratów: ułatwiają rozumowanie na temat zarządzania przestrzenią, ponieważ stare dane muszą być jawnie zachowywane. Rozważać:

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

Jest to dobrze znany wyciek przestrzeni, ponieważ cała lista xsmusi zostać zachowana w pamięci, aby obliczyć zarówno sumi length. Można stworzyć efektywnego konsumenta tworząc fałdę:

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'

Ale zrobienie tego dla każdego procesora strumieniowego jest nieco niewygodne. Istnieją pewne uogólnienia ( Conal Elliott - Beautiful Fold Zipping ), ale wydaje się, że nie przyjęły się. Jednak iteracje mogą zapewnić podobny poziom ekspresji.

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

Nie jest to tak wydajne jak zagięcie, ponieważ lista jest wciąż powtarzana wielokrotnie, jednak jest gromadzona w fragmentach, dzięki czemu stare dane można skutecznie zbierać jako śmieci. Aby złamać tę właściwość, konieczne jest jawne zachowanie całego wejścia, na przykład w przypadku stream2list:

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

Stan iteracji jako modelu programowania jest w toku, jednak jest znacznie lepszy niż jeszcze rok temu. Uczymy co kombinatorów są przydatne (np zip, breakE, enumWith), i które są w mniejszym stopniu, w wyniku czego wbudowanym iteratees i kombinatorów zapewnić stale większą ekspresyjność.

To powiedziawszy, Dons ma rację, że jest to zaawansowana technika; Z pewnością nie użyłbym ich do każdego problemu we / wy.


25

Cały czas używam leniwych operacji wejścia / wyjścia w kodzie produkcyjnym. To tylko problem w pewnych okolicznościach, jak wspomniał Don. Ale wystarczy przeczytać kilka plików, ale działa dobrze.


Używam też leniwych I / O. Zwracam się do iteratów, gdy chcę mieć większą kontrolę nad zarządzaniem zasobami.
John L

20

Aktualizacja: Niedawno w haskell-cafe Oleg Kiseljov pokazał, że unsafeInterleaveST(który jest używany do implementacji leniwego IO w monadzie ST) jest bardzo niebezpieczny - łamie rozumowanie równań. On pokazuje, że pozwala skonstruować bad_ctx :: ((Bool,Bool) -> Bool) -> Bool w taki sposób,

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

mimo że ==jest przemienny.


Kolejny problem z leniwym IO: Faktyczna operacja IO może zostać odroczona, aż będzie za późno, na przykład po zamknięciu pliku. Cytat z Haskell Wiki - Problemy z leniwym IO :

Na przykład częstym błędem początkującego jest zamykanie pliku, zanim skończy się go czytać:

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

Problem polega na tym, że withFile zamyka dojście przed wymuszeniem fileData. Prawidłowym sposobem jest przekazanie całego kodu do withFile:

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

Tutaj dane są zużywane przed zakończeniem withFile.

Jest to często nieoczekiwane i łatwy do popełnienia błąd.


Zobacz też: Trzy przykłady problemów z leniwym we / wy .


Właściwie łączenie hGetContentsi withFilejest bezcelowe, ponieważ ten pierwszy ustawia uchwyt w stan „pseudo-zamknięty” i będzie obsługiwał zamykanie za Ciebie (leniwie), więc kod jest dokładnie równoważny readFilelub nawet openFilebez hClose. To w zasadzie co leniwy I / O jest . Jeśli nie używasz readFile, getContentsczy hGetContentsnie używasz leniwy I / O. Na przykład line <- withFile "test.txt" ReadMode hGetLinedziała dobrze.
Dzień

1
@Dag: chociaż hGetContentszajmie się zamknięciem pliku za Ciebie, dopuszczalne jest również samodzielne zamknięcie go „wcześniej” i pomaga zapewnić przewidywalne zwolnienie zasobów.
Ben Millwood,

17

Innym problemem związanym z leniwym IO, o którym do tej pory nie wspomniano, jest zaskakujące zachowanie. W normalnym programie Haskell czasami trudno jest przewidzieć, kiedy każda część programu jest oceniana, ale na szczęście ze względu na czystość nie ma to znaczenia, chyba że masz problemy z wydajnością. Kiedy wprowadzane jest leniwe IO, kolejność oceny twojego kodu w rzeczywistości ma wpływ na jego znaczenie, więc zmiany, które zwykłeś uważać za nieszkodliwe, mogą spowodować prawdziwe problemy.

Jako przykład, oto pytanie dotyczące kodu, który wygląda rozsądnie, ale jest bardziej zagmatwany przez odroczone IO: withFile vs. openFile

Te problemy nie zawsze są śmiertelne, ale to inna sprawa do przemyślenia i dostatecznie silny ból głowy, którego osobiście unikam leniwego IO, chyba że jest prawdziwy problem z wykonaniem całej pracy z góry.

Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.