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?
źródło
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?
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ównosum
ilength
. 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.
źródło
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.
źródło
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 :
Jest to często nieoczekiwane i łatwy do popełnienia błąd.
Zobacz też: Trzy przykłady problemów z leniwym we / wy .
źródło
hGetContents
iwithFile
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żnyreadFile
lub nawetopenFile
bezhClose
. To w zasadzie co leniwy I / O jest . Jeśli nie używaszreadFile
,getContents
czyhGetContents
nie używasz leniwy I / O. Na przykładline <- 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.
źródło