Cosa c'è di così negativo in Lazy I / O?


89

In genere ho sentito che il codice di produzione dovrebbe evitare di utilizzare Lazy I / O. La mia domanda è: perché? Va mai bene usare Lazy I / O al di fuori del solo gioco? E cosa rende migliori le alternative (es. Enumeratori)?

Risposte:


81

Lazy IO ha il problema che il rilascio di qualsiasi risorsa che hai acquisito è in qualche modo imprevedibile, poiché dipende da come il tuo programma consuma i dati - il suo "modello di domanda". Una volta che il programma elimina l'ultimo riferimento alla risorsa, il GC verrà eseguito e rilascerà quella risorsa.

I flussi pigri sono uno stile molto comodo da programmare. Questo è il motivo per cui le pipe di shell sono così divertenti e popolari.

Tuttavia, se le risorse sono limitate (come negli scenari ad alte prestazioni o negli ambienti di produzione che prevedono di scalare fino ai limiti della macchina) fare affidamento sul GC per ripulire può essere una garanzia insufficiente.

A volte è necessario rilasciare le risorse con entusiasmo, al fine di migliorare la scalabilità.

Quindi quali sono le alternative al pigro IO che non significa rinunciare all'elaborazione incrementale (che a sua volta consumerebbe troppe risorse)? Bene, abbiamo foldlbasato l'elaborazione, alias iterate o enumeratori, introdotta da Oleg Kiselyov alla fine degli anni 2000 e da allora resa popolare da una serie di progetti basati sulla rete.

Invece di elaborare i dati come flussi pigri o in un enorme batch, astraggiamo invece su un'elaborazione rigorosa basata su blocchi, con la finalizzazione garantita della risorsa una volta letto l'ultimo blocco. Questa è l'essenza della programmazione basata su iterazioni e offre vincoli di risorse molto interessanti.

Lo svantaggio dell'IO basato su iterazioni è che ha un modello di programmazione piuttosto scomodo (più o meno analogo alla programmazione basata su eventi, rispetto a un bel controllo basato su thread). È sicuramente una tecnica avanzata, in qualsiasi linguaggio di programmazione. E per la stragrande maggioranza dei problemi di programmazione, lazy IO è del tutto soddisfacente. Tuttavia, se aprirai molti file, o parlerai su molti socket, o altrimenti utilizzerai molte risorse simultanee, un approccio iterativo (o enumeratore) potrebbe avere senso.


22
Dato che ho appena seguito un collegamento a questa vecchia domanda da una discussione sull'I / O pigro, ho pensato di aggiungere una nota che da allora, gran parte dell'imbarazzo degli iterati è stata sostituita da nuove librerie di streaming come pipe e conduit .
Ørjan Johansen

40

Dons ha fornito un'ottima risposta, ma ha tralasciato quella che è (per me) una delle caratteristiche più convincenti degli iterati: rendono più facile ragionare sulla gestione dello spazio perché i vecchi dati devono essere esplicitamente conservati. Ritenere:

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

Questa è una nota perdita di spazio, perché l'intero elenco xsdeve essere conservato in memoria per calcolare sia sume length. È possibile rendere un consumatore efficiente creando una piega:

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'

Ma è un po 'scomodo doverlo fare per ogni processore di flusso. Ci sono alcune generalizzazioni ( Conal Elliott - Beautiful Fold Zipping ), ma non sembrano aver preso piede. Tuttavia, gli iterati possono ottenere un livello di espressione simile.

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

Questo non è efficiente come un fold perché l'elenco è ancora ripetuto più volte, tuttavia viene raccolto in blocchi in modo che i vecchi dati possano essere raccolti in modo efficiente. Per rompere quella proprietà, è necessario conservare esplicitamente l'intero input, come con stream2list:

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

Lo stato degli iterati come modello di programmazione è un work in progress, tuttavia è molto meglio di un anno fa. Stiamo imparando che cosa combinatori sono utili (ad esempio zip, breakE, enumWith) e che lo sono meno, con il risultato che built-in iteratees e combinatori forniscono continuamente più espressività.

Detto questo, Dons è corretto dicendo che sono una tecnica avanzata; Certamente non li userei per ogni problema di I / O.


25

Uso sempre l'I / O pigro nel codice di produzione. È un problema solo in determinate circostanze, come ha detto Don. Ma solo per leggere alcuni file funziona bene.


Uso anche l'I / O pigro. Mi rivolgo agli iterati quando voglio un maggiore controllo sulla gestione delle risorse.
John L

20

Aggiornamento: Recentemente su haskell-cafe Oleg Kiseljov ha dimostrato che unsafeInterleaveST(che viene utilizzato per implementare l'IO pigro all'interno della monade ST) è molto pericoloso - rompe il ragionamento equazionale. Mostra che permette di costruire bad_ctx :: ((Bool,Bool) -> Bool) -> Bool tale che

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

anche se ==è commutativo.


Un altro problema con lazy IO: l'effettiva operazione di I / O può essere differita fino a quando non è troppo tardi, ad esempio dopo la chiusura del file. Citando da Haskell Wiki - Problemi con lazy IO :

Ad esempio, un errore comune per principianti è chiudere un file prima che uno abbia finito di leggerlo:

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

Il problema è che con File chiude l'handle prima che fileData venga forzato. Il modo corretto è passare tutto il codice a withFile:

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

Qui, i dati vengono consumati prima che withFile termini.

Questo è spesso inaspettato e un errore facile da fare.


Vedi anche: Tre esempi di problemi con pigro di I / O .


In realtà combinare hGetContentsed withFileè inutile perché il primo mette la maniglia in uno stato "pseudo-chiuso" e gestirà la chiusura per te (pigramente) quindi il codice è esattamente equivalente a readFile, o anche openFilesenza hClose. Questo è fondamentalmente ciò pigro I / O è . Se non si utilizza readFile, getContentso hGetContentsnon si sta usando pigro I / O. Ad esempio line <- withFile "test.txt" ReadMode hGetLinefunziona bene.
Dag

1
@Dag: sebbene hGetContentsgestirà la chiusura del file per te, è anche consentito chiuderlo da solo "in anticipo" e aiuta a garantire che le risorse vengano rilasciate in modo prevedibile.
Ben Millwood

17

Un altro problema con lazy IO che non è stato menzionato finora è che ha un comportamento sorprendente. In un normale programma Haskell, a volte può essere difficile prevedere quando ogni parte del programma viene valutata, ma fortunatamente a causa della purezza non ha davvero importanza a meno che non si abbiano problemi di prestazioni. Quando viene introdotto lazy IO, l'ordine di valutazione del codice ha effettivamente un effetto sul suo significato, quindi i cambiamenti a cui sei abituato a pensare come innocui possono causare problemi reali.

Ad esempio, ecco una domanda sul codice che sembra ragionevole ma è reso più confuso dall'IO differito: withFile vs. openFile

Questi problemi non sono invariabilmente fatali, ma è un'altra cosa a cui pensare, e un mal di testa sufficientemente forte che evito personalmente IO pigro a meno che non ci sia un vero problema nel fare tutto il lavoro in anticipo.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.