haskell io lazy-evaluation

haskell - ¿Qué hay de malo en Lazy I/O?



io lazy-evaluation (5)

En general, escuché que el código de producción debería evitar el uso de Lazy I / O. Mi pregunta es, ¿por qué? ¿Está bien usar E / S Lazy fuera de solo jugar? ¿Y qué hace que las alternativas (por ejemplo, los enumeradores) sean mejores?


Dons ha proporcionado una muy buena respuesta, pero ha dejado fuera lo que es (para mí) una de las características más atractivas de iteratees: hacen que sea más fácil razonar sobre la gestión del espacio porque los datos antiguos deben conservarse explícitamente. Considerar:

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

Esta es una fuga de espacio bien conocida, porque toda la lista xs debe conservarse en la memoria para calcular tanto la sum como la length . Es posible hacer un consumidor eficiente creando un doblez:

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''

Pero es un poco incómodo tener que hacer esto para cada procesador de flujo. Hay algunas generalizaciones ( Conal Elliott - Beautiful Fold Zipping ), pero no parecen haberse puesto de moda . Sin embargo, iteratees puede obtener un nivel de expresión similar.

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

Esto no es tan eficiente como un fold porque la lista aún se repite varias veces, sin embargo, se recopila en fragmentos para que los datos antiguos se puedan recolectar de manera eficiente. Para romper esa propiedad, es necesario retener explícitamente toda la entrada, como con stream2list:

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

El estado de iterar como un modelo de programación es un trabajo en progreso, sin embargo, es mucho mejor que hace un año. Estamos aprendiendo qué combinators son útiles (por ejemplo, zip , breakE , enumWith ) y que lo son menos, con el resultado de que las iteratees y los combinators incorporados proporcionan continuamente más expresividad.

Dicho esto, Dons tiene razón en que son una técnica avanzada; Ciertamente no los usaría para cada problema de E / S.


Lazy IO tiene el problema de que liberar cualquier recurso que hayas adquirido es algo impredecible, ya que depende de cómo tu programa consume los datos: su "patrón de demanda". Una vez que su programa descarta la última referencia al recurso, el GC eventualmente ejecutará y lanzará ese recurso.

Las corrientes perezosas son un estilo muy conveniente para programar. Por eso, las tuberías de concha son tan divertidas y populares.

Sin embargo, si los recursos están restringidos (como en los escenarios de alto rendimiento o entornos de producción que esperan escalar hasta los límites de la máquina) confiar en el GC para limpiar puede ser una garantía insuficiente.

En ocasiones, debe liberar recursos con entusiasmo para mejorar la escalabilidad.

Entonces, ¿cuáles son las alternativas al IO perezoso que no significa renunciar al procesamiento incremental (que a su vez consumiría demasiados recursos)? Bueno, tenemos procesamiento basado en foldl , aka iteratees o enumeradores, introducido por Oleg Kiselyov a finales de la década de 2000 , y desde popularizado por una serie de proyectos basados ​​en redes.

En lugar de procesar datos como flujos perezosos, o en un lote enorme, en lugar de eso, abstraemos el procesamiento estricto basado en fragmentos, con la finalización garantizada del recurso una vez que se lee el último fragmento. Esa es la esencia de la programación basada en iteratee, y una que ofrece muy buenas restricciones de recursos.

La desventaja de la IO basada en iteratee es que tiene un modelo de programación un tanto incómodo (más o menos análogo a la programación basada en eventos, versus un buen control basado en hilos). Definitivamente es una técnica avanzada, en cualquier lenguaje de programación. Y para la gran mayoría de los problemas de programación, IO lento es completamente satisfactorio. Sin embargo, si va a abrir muchos archivos, hablar en muchos sockets o usar muchos recursos simultáneos, un enfoque iteratee (o enumerador) podría tener sentido.


Otro problema con IO perezoso que no se ha mencionado hasta ahora es que tiene un comportamiento sorprendente. En un programa Haskell normal, a veces puede ser difícil predecir cuándo se evalúa cada parte de tu programa, pero afortunadamente debido a la pureza, realmente no importa a menos que tengas problemas de rendimiento. Cuando se introduce IO perezoso, el orden de evaluación de tu código tiene un efecto en su significado, por lo que los cambios que estás acostumbrado a considerar inofensivos pueden causarte problemas genuinos.

Como ejemplo, aquí hay una pregunta sobre el código que parece razonable pero que se vuelve más confusa por IO diferido: con File vs. openFile

Estos problemas no son invariablemente fatales, pero es otra cosa en que pensar, y un dolor de cabeza lo suficientemente severo como para evitar el IO perezoso a menos que haya un problema real con hacer todo el trabajo por adelantado.


Utilizo la E / S perezosa en el código de producción todo el tiempo. Es solo un problema en ciertas circunstancias, como lo mencionó Don. Pero solo por leer algunos archivos funciona bien.


Actualización: Recientemente, en Haskell-Cafe, Oleg Kiseljov demostró que unsafeInterleaveST (que se usa para implementar IO perezoso dentro de la mónada de ST) es muy inseguro: rompe el razonamiento ecuacional. Él muestra que permite construir bad_ctx :: ((Bool,Bool) -> Bool) -> Bool tal que

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

a pesar de que == es conmutativa.

Otro problema con IO perezosa: la operación IO real se puede posponer hasta que sea demasiado tarde, por ejemplo, después de cerrar el archivo. Citando de Wiki Haskell - Problemas con IO perezoso :

Por ejemplo, un error común de principiante es cerrar un archivo antes de que uno haya terminado de leerlo:

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

El problema es conFile cierra el controlador antes de forzar FileData. La forma correcta es pasar todo el código a withFile:

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

Aquí, los datos se consumen antes conFile finaliza.

Esto a menudo es inesperado y es un error fácil de realizar.

Ver también: tres ejemplos de problemas con Lazy I / O.