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
- Use IO estricto y cargue toda la cadena en la memoria solo para consumirla perezosamente con su analizador. Esto es simple, predecible, pero ineficiente.
- 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,
- Use
pipes
(oconduit
) para construir un sistema de corutinas que incluya su analizadorattoparsec
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 usarpipes
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.