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?
main
un programa Haskell esIO ()
: 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.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.Respuestas:
Ese es el punto donde creo que no lo estás viendo desde la perspectiva de los Haskeller. Entonces tenemos un programa como este:
Creo que una opinión típica de Haskeller sobre esto sería que
convert
, la parte pura:IO
partes;IO
nada.Por lo que no ven esto como
convert
ser "contenida" enIO
, sino más bien, ya que se aisló a partirIO
. De su tipo, lo queconvert
haga nunca puede depender de nada de lo que sucede en unaIO
acción.Yo diría que esto se divide en dos cosas:
convert
depende del estado del archivo.convert
función hace , que no depende del estado del archivo.convert
es 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
convert
de 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,convert
debe manejarlo correctamente. Y el hecho de que la pureza restrinja lo queconvert
puede hacer con su entrada simplifica ese razonamiento.Entonces, si
convert
produce resultados incorrectos de algunos argumentos y loreadFile
alimenta como tal argumento, no lo vemos como un error introducido por el estado . ¡Es un error en una función pura!fuente
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:
Es cierto que la
main
función de Haskell "vive"IO
, pero se alienta a los programas reales de Haskell a no usarlaIO
cuando no sea necesaria. Casi todas las funciones que escribe que no necesitan hacer E / S no deberían tener tipoIO
.Entonces, en su último ejemplo, diría que lo tiene al revés:
main
es impuro (porque lee y escribe archivos) pero las funciones centrales comoconvert
son puras.fuente
¿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:
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,
name
impone 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.
fuente
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()
ywrite_file()
viene perfectamente natural, porque, a pesar deconvert()
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 pararead_file()
ywrite_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()
(digamosread_config_file()
). En Haskell, cuando declaraconvert :: String -> String
o similar, sinIO
mó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 modificarconvert
para 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 pararead_config_file
salirconvert
y transmitir su resultadoconvert
, manteniendo la pureza).fuente