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?
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:
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 foldl
oparte 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.
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 xs
musi zostać zachowana w pamięci, aby obliczyć zarówno sum
i 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.
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.
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 .
hGetContents
i withFile
jest 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 readFile
lub nawet openFile
bez hClose
. To w zasadzie co leniwy I / O jest . Jeśli nie używasz readFile
, getContents
czy hGetContents
nie używasz leniwy I / O. Na przykład line <- withFile "test.txt" ReadMode hGetLine
działa dobrze.
hGetContents
zajmie się zamknięciem pliku za Ciebie, dopuszczalne jest również samodzielne zamknięcie go „wcześniej” i pomaga zapewnić przewidywalne zwolnienie zasobów.
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.