usos una tubo tuberias tipos que las historia diferencia definicion costura con clasificacion cañeria agua haskell pipe conduit haskell-pipes

haskell - tubo - ¿Qué es una tubería/conducto tratando de resolver?



tuberias definicion y clasificacion (3)

He visto personas recomendando pipe / conduit library para varias tareas perezosas relacionadas con IO. ¿Qué problema resuelven estas bibliotecas exactamente?

Además, cuando intento utilizar algunas bibliotecas relacionadas con hacks, es muy probable que haya tres versiones diferentes. Ejemplo:

Esto me confunde. Para mis tareas de análisis debería usar attoparsec o pipes-attoparsec / attoparsec-conduit? ¿Qué beneficio me da la versión de tuberías / conductos en comparación con el attoparsec de vainilla normal?


Lazy IO

Lazy IO funciona así

readFile :: FilePath -> IO ByteString

donde se garantiza que ByteString solo se leerá pedazo por pedazo. Para hacerlo, podríamos (casi) escribir

-- given `readChunk` which reads a chunk beginning at n readChunk :: FilePath -> Int -> IO (Int, ByteString) readFile fp = readChunks 0 where readChunks n = do (n'', chunk) <- readChunk fp n chunks <- readChunks n'' return (chunk <> chunks)

pero aquí observamos que la acción de IO readChunks n'' se realiza antes de devolver incluso el resultado parcial disponible como chunk . Esto significa que no somos flojos en absoluto. Para combatir esto usamos unsafeInterleaveIO

readFile fp = readChunks 0 where readChunks n = do (n'', chunk) <- readChunk fp n chunks <- unsafeInterleaveIO (readChunks n'') return (chunk <> chunks)

lo que hace que readChunks n'' regrese inmediatamente, lo que hace que una acción de IO se realice solo cuando ese thunk es forzado.

Esa es la parte peligrosa: mediante el uso de unsafeInterleaveIO hemos retrasado un conjunto de acciones de IO a puntos no deterministas en el futuro que dependen de cómo consumimos nuestros trozos de ByteString .

Arreglando el problema con corutinas

Lo que nos gustaría hacer es deslizar un paso de procesamiento de fragmento entre la llamada a readChunk y la recursión en readChunks .

readFileCo :: Monoid a => FilePath -> (ByteString -> IO a) -> IO a readFileCo fp action = readChunks 0 where readChunks n = do (n'', chunk) <- readChunk fp n a <- action chunk as <- readChunks n'' action return (a <> as)

Ahora tenemos la posibilidad de realizar acciones IO arbitrarias después de cargar cada fragmento pequeño. Esto nos permite hacer mucho más trabajo de forma incremental sin cargar por completo ByteString en la memoria. Desafortunadamente, no es tremendamente compositivo, tenemos que desarrollar nuestra action consumo y pasarla a nuestro productor ByteString para que se ejecute.

IO a base de tubos

Esto es esencialmente lo que solucionan las pipes : nos permite componer rutinas efectivas con facilidad. Por ejemplo, ahora escribimos nuestro lector de archivos como un Producer que se puede considerar como "transmisión" de los fragmentos del archivo cuando finalmente se ejecuta su efecto.

produceFile :: FilePath -> Producer ByteString IO () produceFile fp = produce 0 where produce n = do (n'', chunk) <- liftIO (readChunk fp n) yield chunk produce n''

Tenga en cuenta las similitudes entre este código y readFileCo anterior, simplemente reemplazamos la llamada a la acción corutina con el yield del chunk que hemos producido hasta ahora. Este llamado a yield genera un tipo de Producer lugar de una acción de IO procesar que podemos componer con otros tipos de Pipe para construir un buen pipeline de consumo llamado Effect IO () .

Toda esta construcción de tuberías se realiza estáticamente sin invocar realmente ninguna de las acciones de IO S. Así es como las pipes permiten escribir sus corutinas más fácilmente. Todos los efectos se activan a la vez cuando llamamos a runEffect en nuestra acción IO main .

runEffect :: Effect IO () -> IO ()

Attoparsec

Entonces, ¿por qué querrías enchufar attoparsec en las pipes ? Bueno, attoparsec está optimizado para el análisis lento. Si está produciendo los trozos alimentados a un analizador attoparsec de manera efectiva, entonces se encontrará en un callejón sin salida. Tú podrías

  1. Use IO estricto y cargue toda la cadena en la memoria solo para consumirla perezosamente con su analizador. Esto es simple, predecible, pero ineficiente.
  2. Use IO lento y pierda la capacidad de razonar sobre cuándo se ejecutarán realmente los efectos de I / O de producción, lo que podría causar posibles fugas de recursos o excepciones de manejo cerradas de acuerdo con el cronograma de consumo de los elementos analizados. Esto es más eficiente que (1) pero puede volverse impredecible fácilmente; o,
  3. Use pipes (o conduit ) para construir un sistema de corutinas que incluya su analizador attoparsec perezoso, lo que le permitirá operar con la menor cantidad de información posible mientras produce los valores analizados de la forma más perezosa posible en toda la secuencia.

Si desea usar attoparsec, use attoparsec

Para mis tareas de análisis debería usar attoparsec o pipes-attoparsec / attoparsec-conduit?

Ambos pipes-attoparsec y attoparsec-conduit transforman un Parser attoparsec dado en un sumidero / conducto o tubería. Por lo tanto, debe usar attoparsec cualquier manera.

¿Qué beneficio me da la versión de tuberías / conductos en comparación con el attoparsec de vainilla normal?

Trabajan con tuberías y conductos, donde el de vainilla no (al menos no de fábrica).

Si no usa conductos o tuberías, y está satisfecho con el rendimiento actual de su IO perezosa, no hay necesidad de cambiar su flujo actual, especialmente si no está escribiendo una gran aplicación o procesando archivos de gran tamaño. Puedes simplemente usar attoparsec .

Sin embargo, eso supone que conoce los inconvenientes de IO perezoso.

¿Cuál es el problema con IO perezoso? (Estudio de problemas con el withFile )

No olvidemos su primera pregunta:

¿Qué problema resuelven estas bibliotecas exactamente?

Resuelven el problema de transmisión de datos (ver 1 y 3 ), que ocurre dentro de los lenguajes funcionales con IO perezoso. Lazy IO a veces no le da lo que desea (vea el ejemplo a continuación), y en ocasiones es difícil determinar los recursos reales del sistema necesarios para una operación específica (se leen / escriben los datos en trozos / bytes / en búfer / onclose / onopen ...) .

Ejemplo de holgazanería

import System.IO main = withFile "myfile" ReadMode hGetContents >>= return . (take 5) >>= putStrLn

Esto no imprimirá nada, ya que la evaluación de los datos ocurre en putStrLn , pero el identificador ya se ha cerrado en este punto.

Arreglando el fuego con ácido venenoso

Si bien el siguiente fragmento corrige esto, tiene otra característica desagradable:

main = withFile "myfile" ReadMode $ /handle -> hGetContents handle >>= return . (take 5) >>= putStrLn

En este caso, hGetContents leerá todo el archivo , algo que no esperabas al principio. Si solo desea comprobar los bytes mágicos de un archivo que podría tener varios GB de tamaño, este no es el camino a seguir.

Usar withFile correctamente

La solución es, obviamente, take las cosas en el contexto withFile :

main = withFile "myfile" ReadMode $ /handle -> fmap (take 5) (hGetContents handle) >>= putStrLn

Esto es, por cierto, también la solución 1 :

Esto [..] responde a una pregunta que a veces me hacen las personas sobre las pipes , que voy a parafrasear aquí:

Si la gestión de recursos no es un foco central de las pipes , ¿por qué debería usar pipes lugar de IO lento?

Muchas personas que hacen esta pregunta descubrieron la programación de flujo a través de Oleg, quien enmarcó el problema perezoso de IO en términos de gestión de recursos. Sin embargo, nunca encontré este argumento convincente de forma aislada; puede resolver la mayoría de los problemas de administración de recursos simplemente separando la adquisición de recursos del IO perezoso, así: [vea el último ejemplo anterior]

Lo que nos lleva de vuelta a mi declaración anterior:

Simplemente puede usar attoparsec [...] [con IO perezoso, asumiendo] que conoce los inconvenientes de IO perezoso.

Referencias

  • 3 , que explica mejor el ejemplo y proporciona una mejor visión general
  • Gabriel Gonzalez (mantenedor / autor de pipes): 1
  • Michael Snoyman (mantenedor / autor del conducto): Conducto versus Enumerador

Aquí hay un gran podcast con autores de ambas bibliotecas:

http://www.haskellcast.com/episode/006-gabriel-gonzalez-and-michael-snoyman-on-pipes-and-conduit/

Responderá la mayoría de tus preguntas.

En resumen, ambas bibliotecas abordan el problema del streaming, que es muy importante cuando se trata de IO. En esencia, gestionan la transferencia de datos en fragmentos, lo que le permite, por ejemplo, transferir un archivo de 1 GB que consume solo 64 KB de RAM tanto en el servidor como en el cliente. Sin transmisión, habría tenido que asignar tanta memoria en ambos extremos.

Una alternativa más antigua a esas bibliotecas es la IO lenta, pero está llena de problemas y hace que las aplicaciones sean propensas a errores. Esos temas se discuten en el podcast.

Con respecto a cuál de esas bibliotecas usar, es más una cuestión de gusto. Prefiero "pipas". Las diferencias detalladas también se tratan en el podcast.