haskell - online - ¿Qué hace que valga la pena la complejidad de Iteratees?
haskell pdf (3)
Bajo qué circunstancias sus beneficios justifican su complejidad.
Cada idioma tiene una IO estricta (clásica), donde todos los recursos son administrados por el usuario. Haskell también proporciona E / S perezosa en todas partes, donde toda la administración de recursos se delega en el sistema.
Sin embargo, eso puede crear problemas, ya que el alcance de los recursos depende de las propiedades de demanda del tiempo de ejecución.
Iteratees golpean una tercera vía:
- Abstracciones de alto nivel, como perezoso IO.
- Alcance explícito, léxico de recursos, como IO estricto.
Se justifica cuando tiene tareas complejas de procesamiento de IO, pero límites muy estrictos en el uso de recursos. Un ejemplo es un servidor web.
De hecho, Snap se basa en iterare IO encima de epoll.
En primer lugar, entiendo el modo de iterar, lo suficientemente bien como para escribir una implementación simplista y con errores sin tener que remitirme a ninguna existente.
Lo que realmente me gustaría saber es por qué las personas parecen encontrarlos tan fascinantes, o en qué circunstancias sus beneficios justifican su complejidad. Comparándolos con la E / S perezosa hay un beneficio muy claro, pero eso se parece mucho a un hombre de paja para mí. En primer lugar, nunca me sentí cómodo con la I / O perezosa, y la evito, excepto por los ocasionales contenidos de hGetContents
o readFile
, principalmente en programas muy simples.
En situaciones reales, generalmente utilizo interfaces de E / S tradicionales con abstracciones de control apropiadas para la tarea. En ese contexto, simplemente no veo el beneficio de las iteraciones, o para qué tareas son una abstracción de control apropiada. La mayoría de las veces parecen más una complejidad innecesaria o incluso una inversión de control contraproducente.
He leído un buen número de artículos sobre ellos y fuentes que los utilizan, pero aún no he encontrado un ejemplo convincente que realmente me haya hecho pensar en algo como "oh, sí, los habría usado allí también . " Tal vez simplemente no he leído los correctos. O quizás haya una interfaz aún por diseñar, más simple que cualquiera que haya visto, que los haga sentir menos como una motosierra suiza.
¿Estoy sufriendo de un síndrome no inventado o mi malestar está bien fundamentado? ¿O es acaso algo completamente distinto?
En cuanto a por qué las personas los encuentran tan fascinantes, creo que son una idea tan simple. La reciente discusión en Haskell-cafe sobre una semántica de denotación para iterativos se convirtió en un consenso de que son tan simples que apenas vale la pena describirlos. La frase "poco más que un pliegue glorificado a la izquierda con un botón de pausa" sobresale de ese hilo. A las personas que les gusta Haskell les suelen gustar las estructuras simples y elegantes, por lo que es probable que la idea sea muy atractiva.
Para mí, los principales beneficios de iterate son
- Composabilidad. No solo se pueden componer las iteraciones, sino también los enumeradores. Esto es muy poderoso.
- Uso seguro de los recursos. Los recursos (memoria y manejadores en su mayoría) no pueden escapar de su alcance local. Compare con la estricta E / S, donde es más fácil crear fugas de espacio al no limpiar.
- Eficiente. Los iterados pueden ser altamente eficientes; competitivo con o mejor que la E / S perezosa y la E / S estricta.
Descubrí que las iteraciones proporcionan los mayores beneficios cuando se trabaja con datos lógicos únicos que provienen de múltiples fuentes. Esto es cuando la capacidad de composición es más útil, y la administración de recursos con I / O estricta es la más molesta (por ejemplo, las alloca
anidadas o los bracket
).
Por ejemplo, en un editor de audio de trabajo en progreso, una sola porción lógica de datos de sonido es un conjunto de compensaciones en múltiples archivos de audio. Puedo procesar ese fragmento de sonido haciendo algo como esto (de memoria, pero creo que es correcto):
enumSound :: MonadIO m => Sound -> Enumerator s m a
enumSound snd = foldr (>=>) enumEof . map enumFile $ sndFiles snd
Esto me parece claro, conciso y elegante, mucho más que la I / O estricta equivalente. Los iterados también son lo suficientemente poderosos como para incorporar cualquier procesamiento que quiera hacer, incluida la salida de escritura, por lo que me parece muy agradable. Si utilizara la I / O perezosa podría obtener algo tan elegante, pero el cuidado adicional para asegurar que los recursos se consuman y GC''d superaría las ventajas de la OMI.
También me gusta que necesite retener explícitamente los datos en iteraciones, lo que evita la notoria mean xs = sum xs / length xs
fuga de espacio.
Por supuesto, no uso iteratees para todo. Como alternativa, realmente me gusta el idioma with*
, pero cuando tiene varios recursos que deben ser anidados, esto se complica muy rápidamente.
Esencialmente, se trata de hacer IO en un estilo funcional, correcta y eficientemente . Eso es todo, de verdad.
Correctos y eficientes son bastante fáciles de usar con un estilo cuasi imperativo con estricto IO. El estilo funcional es fácil con la IO perezosa, pero es técnicamente engañoso (usando unsafeInterleaveIO
debajo del capó) y puede tener problemas con la administración y la eficiencia de los recursos.
En términos muy, muy generales, una gran cantidad de código funcional puro sigue un patrón de tomar algunos datos, expandiéndolos recursivamente en partes más pequeñas, transformando las piezas de alguna manera, y luego recombinándolas en un resultado final. La estructura puede ser implícita (en el gráfico de llamadas del programa) o una estructura de datos explícita que se está atravesando.
Pero esto se desmorona cuando IO está involucrado. Digamos que sus datos iniciales son un identificador de archivo, el paso de "expansión recursiva" es leer una línea desde allí, y no puede leer todo el archivo en la memoria de una vez. Esto obliga a que todo el proceso de lectura-transformación-recombinación se realice para cada línea antes de leer la siguiente, por lo que en lugar de la estructura limpia "desplegar, desplegar, plegar, plegar" se combinan en funciones monádicas explícitamente recursivas utilizando IO estricta.
Los iterados proporcionan una estructura alternativa para resolver el mismo problema. Los pasos de "transformar y recombinar" se extraen y, en lugar de ser funciones , se cambian a una estructura de datos que representa el estado actual del cálculo. El paso de "expansión recursiva" recibe la responsabilidad de obtener los datos y enviarlos a una iteración (de lo contrario pasiva).
¿Qué beneficios ofrece esta oferta? Entre otras cosas:
- Debido a que un iteratee es un objeto pasivo que realiza pasos individuales de un cálculo, se pueden componer fácilmente de diferentes maneras, por ejemplo, intercalando dos iteradas en lugar de ejecutarlas secuencialmente.
- La interfaz entre iteradores y enumeradores es pura, solo se procesa un flujo de valores, por lo que una función pura se puede unir libremente entre ellos.
- Las fuentes de datos y los cálculos son ajenos al funcionamiento interno de cada uno, al desacoplar la entrada y la gestión de recursos del procesamiento y la salida.
El resultado final es que un programa puede tener una estructura de alto nivel mucho más cercana a lo que sería una versión funcional pura, con muchos de los mismos beneficios para la composición, mientras que al mismo tiempo tiene una eficiencia comparable a la versión de OI más imperativa y estricta.
¿En cuanto a ser "digno de la complejidad"? Bueno, esa es la cuestión: realmente no son tan complejos, solo un poco nuevos y desconocidos. La idea ha estado flotando por solo, ¿qué, un par de años? Dale algo de tiempo para que las cosas se desestabilicen a medida que las personas usan IO basadas en iteraciones en proyectos más grandes (por ejemplo, con cosas como Snap) y para que aparezcan más ejemplos / tutoriales. Es probable que, en retrospectiva, las implementaciones actuales parezcan muy aproximadas.
Algo relacionado: es posible que desee leer esta discusión sobre IO de estilo funcional . Los iterados no se mencionan demasiado, pero el problema central es muy similar. En particular, esta solución , que es a la vez muy elegante y va incluso más allá de la iteración en la abstracción de IO incremental.