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)?
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:
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 foldl
basato 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.
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 xs
deve essere conservato in memoria per calcolare sia sum
e 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.
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.
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 .
hGetContents
ed 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 openFile
senza hClose
. Questo è fondamentalmente ciò pigro I / O è . Se non si utilizza readFile
, getContents
o hGetContents
non si sta usando pigro I / O. Ad esempio line <- withFile "test.txt" ReadMode hGetLine
funziona bene.
hGetContents
gestirà 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.
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.