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

17

Lo siento por otra pregunta de efectos secundarios de FP +, pero no pude encontrar una ya existente que me haya respondido bastante.

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

También reúno el enfoque de Haskell para esto, la mónada IO, lo logra envolviendo acciones con estado en un contenedor, para su posterior ejecución, considerado fuera del alcance del programa en sí.

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

Ejemplo de entrada de crudo.

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,
)

luego, se exime a sí mismo de la responsabilidad al no llamar esos pasos, sino dejar 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 espero que alguien más llame a inner() y dice que tu 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 todavía tendremos los mismos errores relacionados con el estado que la implementación imperativa, así que realmente has ganado algo. como un programador que mantendrá esto?

Aprecio totalmente el beneficio de reducir y aislar el comportamiento con estado, que es de hecho la razón por la que estructuré la versión imperativa de esta manera: recopilar entradas, hacer cosas puras, escupir salidas de salida. Con suerte, convert() puede ser completamente puro y obtener los beneficios de almacenamiento en caché, seguridad de subprocesos, 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 la mónada IO, que me falta?

    
pregunta Stu Cox 06.08.2015 - 01:43

4 respuestas

14
  

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

Esa es la parte en la que creo que no lo estás viendo desde la perspectiva de los Haskeller. Así que 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 típica asunción de Haskeller sobre esto sería que convert , la parte pura:

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

Por lo tanto, no ven que convert está "contenido" en IO , sino que, al estar aislado de IO . De su tipo, lo que haga convert nunca puede depender de nada que suceda en una acción IO .

  

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

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

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

Este es un punto un tanto abstracto, pero es realmente clave para lo que los Haskellers quieren decir cuando hablan de esto. Desea escribir convert de tal manera que dado un cualquier argumento válido, producirá 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; todo lo que importa es que sea cual sea el argumento que se le envíe y de donde provenga, convert debe manejarlo correctamente. Y el hecho de que la pureza restringe lo que convert puede hacer con su entrada simplifica ese razonamiento.

Entonces, si convert produce resultados incorrectos de algunos argumentos, y readFile lo alimenta, no lo vemos como un error introducido por state . ¡Es un error en una función pura!

    
respondido por el sacundim 12.09.2015 - 20:49
7

Es difícil estar seguro de lo que quieres decir con "puramente académico", pero creo que la respuesta es en su mayoría "no".

Como se explica en Abordando al escuadrón torpe por Simon Peyton Jones ( recomendó fuertemente leer!), la E / S monádica estaba destinada a resolver problemas reales con la forma en que Haskell solía manejar la E / S. Lea el ejemplo del servidor con Solicitudes y respuestas, que no copiaré aquí; Es muy instructivo.

A diferencia de Python, Haskell fomenta un estilo de cálculo "puro" que puede ser aplicado por su sistema de tipos. 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 las bibliotecas comunes), la E / S monádica es probablemente menos útil en Python. La filosofía del lenguaje no pretende imponer una separación estricta pura / impura.

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

Otra cosa. Usted dice:

  

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

Es cierto que la función Haskell main "vive" en IO , pero se recomienda que los programas reales de Haskell no utilicen IO cuando no sea necesario. Casi todas las funciones que escribes que no necesitan hacer I / O no deberían tener el tipo IO .

Diría que en tu último ejemplo lo obtuviste al revés: main es impuro (porque lee y escribe archivos) pero las funciones básicas como convert son puras.

    
respondido por el Andres F. 06.08.2015 - 04:15
3

¿Por qué es impuro IO? Porque puede devolver diferentes valores en diferentes momentos. Existe una dependencia en el tiempo que debe se debe tener 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 indicador? No hay nada que dependa de ello, por lo que una evaluación perezosa significa que nunca se exigirá Además, no hay nada que obligue a la solicitud de salida antes de que se lea la entrada. En lo que respecta a la computadora, sin una mónada IO, esas dos primeras expresiones son completamente independientes entre sí. Afortunadamente, name impone un pedido en los dos segundos.

Hay otras formas de resolver el problema de la dependencia del orden, pero usar una mónada IO es probablemente la forma más simple (desde el punto de vista del lenguaje al menos) para permitir que todo permanezca en el reino funcional perezoso, sin pequeñas secciones de Código imperativo. También es el más flexible. Por ejemplo, puede crear un canal de IO de forma relativamente fácil y dinámicamente en tiempo de ejecución según las aportaciones del usuario.

    
respondido por el Karl Bielefeldt 06.08.2015 - 05:51
1
  

Mi (limitada) comprensión de la programación funcional es ese estado / efectos secundarios   debe minimizarse y mantenerse separado de la lógica sin estado.

Eso no es solo programación funcional; Eso suele ser una buena idea en cualquier idioma. Naturalmente, si realiza pruebas de unidad, la forma en que separa las partes read_file() , convert() y write_file() es muy clara porque, a pesar de que convert() es, con mucho, la parte más compleja y grande del código, escribir pruebas es relativamente fácil: todo lo que necesita 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í 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 esas funciones tan simples que te sientas cómodo sin probarlas y así te ahorras un montón de problemas.

La diferencia entre Python y Haskell aquí es que Haskell tiene un comprobador de tipos 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 escritura o lectura de archivos en convert() (por ejemplo, read_config_file() ). En Haskell cuando declara convert :: String -> String o similar, sin IO monad, el comprobador de tipos 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 convert para leer un archivo de configuración, verán rápidamente los errores del compilador que muestran que estarían rompiendo la pureza de la función. (Y espero que sean lo suficientemente sensatos como para mover read_config_file de convert y pasar su resultado a convert , manteniendo la pureza).

    
respondido por el Curt J. Sampson 06.12.2018 - 03:22

Lea otras preguntas en las etiquetas