¿Es el beneficio del patrón IO mónada para el manejo de efectos secundarios puramente académico?

17

Perdón por otra pregunta de efectos secundarios de FP +, pero no pude encontrar una existente que respondiera esto por mí.

Mi comprensión (limitada) de la programación funcional es que los efectos de estado / secundarios deben minimizarse y mantenerse separados de la lógica sin estado.

También deduzco que el enfoque de Haskell sobre esto, la mónada IO, logra esto envolviendo acciones con estado en un contenedor, para su posterior ejecución, considerado fuera del alcance del propio programa.

Estoy tratando de entender este patrón, pero en realidad para determinar si usarlo en un proyecto de Python, así que quiero evitar los detalles de Haskell si es posible.

Crudo ejemplo entrante.

Si mi programa convierte un archivo XML en un archivo JSON:

def main():
    xml_data = read_file('input.xml')  # impure
    json_data = convert(xml_data)  # pure
    write_file('output.json', json_data) # impure

¿No es el enfoque de la mónada IO para hacer esto de manera efectiva?

steps = list(
    read_file,
    convert,
    write_file,
)

entonces, ¿se absuelve de la responsabilidad al no llamar a esos pasos, sino al permitir que el intérprete lo haga?

O dicho de otra manera, es como escribir:

def main():  # pure
    def inner():  # impure
        xml_data = read_file('input.xml')
        json_data = convert(xml_data)
        write_file('output.json', json_data)
    return inner

luego esperar que alguien más llame inner()y decir que su trabajo está hecho porque main()es puro.

Todo el programa terminará contenido en la mónada IO, básicamente.

Cuando el código se ejecuta realmente , todo después de leer el archivo depende del estado de ese archivo, por lo que aún sufrirá los mismos errores relacionados con el estado que la implementación imperativa, por lo que ¿ha ganado algo como programador que mantendrá esto?

Aprecio totalmente el beneficio de reducir y aislar el comportamiento con estado, de hecho, es por eso que estructuré la versión imperativa de esa manera: reunir entradas, hacer cosas puras, escupir salidas. Con suerte convert()puede ser completamente puro y cosechar los beneficios de la capacidad de almacenamiento en caché, seguridad de hilos, etc.

También aprecio que los tipos monádicos pueden ser útiles, especialmente en tuberías que operan en tipos comparables, pero no veo por qué IO debería usar mónadas a menos que ya estén en esa tubería.

¿Hay algún beneficio adicional al tratar con los efectos secundarios que trae el patrón de mónada IO, que me estoy perdiendo?

Stu Cox
fuente
1
Deberías ver este video . Las maravillas de las mónadas finalmente se revelan sin recurrir a la teoría de la categoría o Haskell. Resulta que las mónadas se expresan trivialmente en JavaScript y son uno de los habilitadores clave de Ajax. Las mónadas son increíbles. Son cosas simples, implementadas casi trivialmente, con un enorme poder para gestionar la complejidad. Pero comprenderlos es sorprendentemente difícil, y la mayoría de las personas, una vez que tienen ese momento ah-ha, parecen perder la capacidad de explicárselos a otros.
Robert Harvey
Buen video, gracias. De hecho, aprendí sobre estas cosas desde una introducción de JS a la programación funcional (luego leí un millón más ...). Aunque he visto eso, estoy bastante seguro de que mi pregunta es específica de la mónada IO, que Crock no cubre en ese video.
Stu Cox
Hmm ... ¿No se considera AJAX una forma de E / S?
Robert Harvey
1
Tenga en cuenta que el tipo de mainun programa Haskell es IO (): una acción de E / S. Esto no es realmente una función en absoluto; Es un valor . Todo el programa es un valor puro que contiene instrucciones que le indican al lenguaje en tiempo de ejecución lo que debe hacer. Todas las cosas impuras (que realmente realizan las acciones de IO) están fuera del alcance de su programa.
Wyzard --Detener Dañar a Mónica--
En su ejemplo, la parte monádica es cuando toma el resultado de un cálculo ( read_file) y lo usa como argumento para el siguiente ( write_file). Si solo tuvieras una secuencia de acciones independientes, no necesitarías una Mónada.
lortabac

Respuestas:

14

Todo el programa terminará contenido en la mónada IO, básicamente.

Ese es el punto donde creo que no lo estás viendo desde la perspectiva de los Haskeller. Entonces tenemos un programa como este:

module Main

main :: IO ()
main = do
  xmlData <- readFile "input.xml"
  let jsonData = convert xmlData
  writeFile "output.json" jsonData

convert :: String -> String
convert xml = ...

Creo que una opinión típica de Haskeller sobre esto sería que convert , la parte pura:

  1. Es probablemente la mayor parte de este programa, y ​​mucho más complicado que el IO partes;
  2. Se puede razonar y probar sin tener que lidiar con IO nada.

Por lo que no ven esto como convertser "contenida" en IO, sino más bien, ya que se aisló a partir IO. De su tipo, lo que converthaga nunca puede depender de nada de lo que sucede en una IOacción.

Cuando el código se ejecuta realmente, todo después de leer el archivo depende del estado de ese archivo, por lo que aún sufrirá los mismos errores relacionados con el estado que la implementación imperativa, por lo que ¿ha ganado algo, como programador que mantendrá esto?

Yo diría que esto se divide en dos cosas:

  1. Cuando el programa se ejecuta, el valor del argumento de que convertdepende del estado del archivo.
  2. Pero lo que la convertfunción hace , que no depende del estado del archivo. convertes siempre la misma función , incluso si se invoca con diferentes argumentos en diferentes puntos.

Este es un punto algo abstracto, pero es realmente clave para lo que Haskellers quieren decir cuando hablan de esto. Desea escribir convertde tal manera que, dado cualquier argumento válido, produzca un resultado correcto para ese argumento. Cuando lo miras así, el hecho de que leer un archivo es una operación con estado no entra en la ecuación; lo único que importa es que cualquier argumento que se le presente y de donde sea que haya venido, convertdebe manejarlo correctamente. Y el hecho de que la pureza restrinja lo que convertpuede hacer con su entrada simplifica ese razonamiento.

Entonces, si convertproduce resultados incorrectos de algunos argumentos y lo readFilealimenta como tal argumento, no lo vemos como un error introducido por el estado . ¡Es un error en una función pura!

sacundim
fuente
Creo que esta es la mejor descripción (aunque los otros también me ayudaron a aclarar las cosas), gracias.
Stu Cox
¿Vale la pena señalar que el uso de mónadas en Python puede tener menos beneficios ya que Python solo tiene un tipo (estático) y, por lo tanto, no garantiza nada?
jk.
7

Es difícil estar seguro exactamente a qué se refiere con "puramente académico", pero creo que la respuesta es principalmente "no".

Como se explica en Hacer frente al pelotón de los torpes por Simon Peyton Jones ( fuertemente lectura recomendada!), Yo monádico O estaba destinado / a resolver problemas reales con la forma en Haskell utiliza para manejar E / S. Lea el ejemplo del servidor con Solicitudes y respuestas, que no copiaré aquí; Es muy instructivo.

Haskell, a diferencia de Python, fomenta un estilo de computación "pura" que su sistema de tipos puede aplicar. Por supuesto, puede usar la autodisciplina al programar en Python para cumplir con este estilo, pero ¿qué pasa con los módulos que no escribió? Sin mucha ayuda del sistema de tipos (y bibliotecas comunes), la E / S monádica es probablemente menos útil en Python. La filosofía del lenguaje no pretende imponer una estricta separación pura / impura.

Tenga en cuenta que esto dice más sobre las diferentes filosofías de Haskell y Python que sobre cuán académica es la E / S monádica. No lo usaría para Python.

Otra cosa. Tu dices:

Todo el programa terminará contenido en la mónada IO, básicamente.

Es cierto que la mainfunción de Haskell "vive" IO, pero se alienta a los programas reales de Haskell a no usarla IOcuando no sea necesaria. Casi todas las funciones que escribe que no necesitan hacer E / S no deberían tener tipo IO.

Entonces, en su último ejemplo, diría que lo tiene al revés: maines impuro (porque lee y escribe archivos) pero las funciones centrales como convertson puras.

Andres F.
fuente
3

¿Por qué es IO impuro? Porque puede devolver diferentes valores en diferentes momentos. Existe una dependencia del tiempo que debe tenerse en cuenta, de una forma u otra. Esto es aún más crucial con la evaluación perezosa. Considere el siguiente programa:

main = do  
    putStrLn "Please enter your name"  
    name <- getLine
    putStrLn $ "Hello, " ++ name

Sin una mónada de E / S, ¿por qué se generaría el primer mensaje? No hay nada que dependa de ello, por lo que una evaluación perezosa significa que nunca se exigirá. Tampoco hay nada que obligue a que se envíe la solicitud antes de leer la entrada. En lo que respecta a la computadora, sin una mónada IO, esas dos primeras expresiones son completamente independientes entre sí. Afortunadamente, nameimpone una orden a los segundos dos.

Hay otras formas de resolver el problema de la dependencia del orden, pero el uso de una mónada de E / S es probablemente la forma más simple (al menos desde el punto de vista del lenguaje) para permitir que todo permanezca en el reino funcional vago, sin pequeñas secciones de código imperativo. También es el más flexible. Por ejemplo, puede construir relativamente fácilmente una tubería de E / S dinámicamente en tiempo de ejecución en función de la entrada del usuario.

Karl Bielefeldt
fuente
2

Mi comprensión (limitada) de la programación funcional es que los efectos de estado / secundarios deben minimizarse y mantenerse separados de la lógica sin estado.

Eso no es solo programación funcional; Esa suele ser una buena idea en cualquier idioma. Si lo hace la unidad de pruebas, la forma en que se partió read_file(), convert()y write_file()viene perfectamente natural, porque, a pesar de convert()que es con mucho la parte más compleja y la más grande del código, escribir pruebas de que es relativamente fácil: todo lo que necesita para configurar es el parámetro de entrada . Escribir pruebas para read_file()y write_file()es un poco más difícil (aunque las funciones en sí mismas son casi triviales) porque necesita crear y / o leer cosas en el sistema de archivos antes y después de llamar a la función. Lo ideal sería hacer que tales funciones sean tan simples que se sienta cómodo al no probarlas y, por lo tanto, ahorrarse mucha molestia.

La diferencia entre Python y Haskell aquí es que Haskell tiene un verificador de tipo que puede probar que las funciones no tienen efectos secundarios. En Python, debe esperar que nadie haya caído accidentalmente en una función de lectura o escritura de archivos convert()(digamos read_config_file()). En Haskell, cuando declara convert :: String -> Stringo similar, sin IOmónada, el verificador de tipo garantizará que esta es una función pura que se basa únicamente en su parámetro de entrada y nada más. Si alguien intenta modificar convertpara leer un archivo de configuración, verá rápidamente los errores del compilador que muestran que están rompiendo la pureza de la función. (Y es de esperar que sean lo suficientemente sensibles como para read_config_filesalir converty transmitir su resultado convert, manteniendo la pureza).

Curt J. Sampson
fuente