¿Por qué se modelan los efectos secundarios como mónadas en Haskell?

¿Podría alguien dar algunos consejos sobre por qué los cálculos impuros en Haskell se modelan como mónadas?

Me refiero a que la mónada es solo una interfaz con 4 operaciones, así que ¿cuál fue el razonamiento para modelar los efectos secundarios en ella?

Supongamos que una función tiene efectos secundarios. Si tomamos todos los efectos que produce como parámetros de entrada y salida, entonces la función es pura para el mundo exterior.

Entonces para una función impura

 f' :: Int -> Int 

añadimos el RealWorld a la consideración

 f :: Int -> RealWorld -> (Int, RealWorld) -- input some states of the whole world, -- modify the whole world because of the a side effects, -- then return the new world. 

entonces f es puro de nuevo. Definimos un tipo de datos parametrizado IO a = RealWorld -> (a, RealWorld) , por lo que no necesitamos escribir RealWorld tantas veces

 f :: Int -> IO Int 

Para el progtwigdor, manejar un RealWorld directamente es demasiado peligroso; en particular, si un progtwigdor tiene en sus manos un valor de tipo RealWorld, podrían intentar copiarlo , lo cual es básicamente imposible. (Piense en intentar copiar todo el sistema de archivos, por ejemplo. ¿Dónde lo colocaría?) Por lo tanto, nuestra definición de IO también encapsula los estados del mundo entero.

Estas funciones impuras son inútiles si no podemos encadenarlas. Considerar

 getLine :: IO String = RealWorld -> (String, RealWorld) getContents :: String -> IO String = String -> RealWorld -> (String, RealWorld) putStrLn :: String -> IO () = String -> RealWorld -> ((), RealWorld) 

Queremos obtener un nombre de archivo de la consola, leer ese archivo y luego imprimir el contenido. ¿Cómo lo haríamos si podemos acceder a los estados del mundo real?

 printFile :: RealWorld -> ((), RealWorld) printFile world0 = let (filename, world1) = getLine world0 (contents, world2) = (getContents filename) world1 in (putStrLn contents) world2 -- results in ((), world3) 

Vemos un patrón aquí: las funciones se llaman así:

 ... (, worldY) = f worldX (, worldZ) = g  worldY ... 

Entonces, podríamos definir un operador ~~~ para unirlos:

 (~~~) :: (IO b) -> (b -> IO c) -> IO c (~~~) :: (RealWorld -> (b, RealWorld)) -> (b -> RealWorld -> (c, RealWorld)) -> RealWorld -> (c, RealWorld) (f ~~~ g) worldX = let (resF, worldY) = f worldX in g resF worldY 

entonces podríamos simplemente escribir

 printFile = getLine ~~~ getContents ~~~ putStrLn 

sin tocar el mundo real.


Ahora supongamos que también queremos que el contenido del archivo sea mayúscula. Uppercasing es una función pura

 upperCase :: String -> String 

Pero para llegar al mundo real, tiene que devolver una IO String . Es fácil levantar tal función:

 impureUpperCase :: String -> RealWorld -> (String, RealWorld) impureUpperCase str world = (upperCase str, world) 

esto puede ser generalizado:

 impurify :: a -> IO a impurify :: a -> RealWorld -> (a, RealWorld) impurify a world = (a, world) 

de modo que impureUpperCase = impurify . upperCase impureUpperCase = impurify . upperCase , y podemos escribir

 printUpperCaseFile = getLine ~~~ getContents ~~~ (impurify . upperCase) ~~~ putStrLn 

(Nota: normalmente escribimos getLine ~~~ getContents ~~~ (putStrLn . upperCase) )


Ahora veamos lo que hemos hecho:

  1. Definimos un operador (~~~) :: IO b -> (b -> IO c) -> IO c que encadena dos funciones impuras juntas
  2. impurify :: a -> IO a una función impurify :: a -> IO a que convierte un valor puro en impuro.

Ahora hacemos la identificación (>>=) = (~~~) y return = impurify , y vemos? Tenemos una mónada.


(Para verificar si realmente es una mónada, hay pocos axiomas que deben cumplirse:

(1) return a >>= f = fa

  impurify a = (\world -> (a, world)) (impurify a ~~~ f) worldX = let (resF, worldY) = (\world -> (a, world)) worldX in f resF worldY = let (resF, worldY) = (a, worldX)) in f resF worldY = fa worldX 

(2) f >>= return = f

  (f ~~~ impurify) a worldX = let (resF, worldY) = impuify a worldX in f resF worldY = let (resF, worldY) = (a, worldX) in f resF worldY = fa worldX 

(3) f >>= (\x -> gx >>= h) = (f >>= g) >>= h

Ejercicio.)

¿Podría alguien dar algunos consejos sobre por qué los cálculos poco convincentes en Haskell se modelan como mónadas?

Esta pregunta contiene un malentendido generalizado. La impureza y la mónada son nociones independientes. La impureza no está modelada por Monad. Por el contrario, hay algunos tipos de datos, como IO , que representan cálculos imperativos. Y para algunos de esos tipos, una pequeña fracción de su interfaz corresponde al patrón de interfaz llamado “Monad”. Además, no se conoce una explicación pura / funcional / denotativa de IO (y es improbable que exista una, teniendo en cuenta el propósito “sin bin” de IO ), aunque existe la historia comúnmente contada sobre World -> (a, World) siendo el significado de IO a . Esa historia no puede describir IO , porque IO admite concurrencia y no determinismo. La historia ni siquiera funciona cuando se trata de cálculos determinísticos que permiten una interacción a medio cómputo con el mundo.

Para más explicación, mira esta respuesta .

Editar : Al volver a leer la pregunta, no creo que mi respuesta esté bien encaminada. Los modelos de cálculo imperativo a menudo resultan ser mónadas, tal como decía la pregunta. El asker podría no asumir realmente que la mónada de ninguna manera permite el modelado de cálculos imperativos.

Tal como lo entiendo, alguien llamado Eugenio Moggi notó por primera vez que una construcción matemática previamente desconocida llamada “mónada” podría usarse para modelar efectos secundarios en lenguajes de progtwigción y, por lo tanto, especificar su semántica usando el cálculo Lambda. Cuando Haskell se estaba desarrollando hubo varias formas en las que se modelaron cómputos impuros (para más detalles, véase el “papel de camisa” de Simon Peyton Jones), pero cuando Phil Wadler introdujo las mónadas rápidamente se hizo obvio que esta era La respuesta. Y el rest es historia.

¿Podría alguien dar algunos consejos sobre por qué los cálculos poco convincentes en Haskell se modelan como mónadas?

Bueno, porque Haskell es puro . Se necesita un concepto matemático para distinguir entre cálculos puros y puros en el nivel de tipo y para modelar flujos de progtwig en respectivamente.

Esto significa que tendrá que terminar con algún tipo IO a que modele un cálculo no preciso. Entonces necesitas saber cómo combinar estos cálculos, los cuales se aplican en secuencia ( >>= ) y levantar un valor ( return ) son los más obvios y básicos.

Con estos dos, ya has definido una mónada (sin siquiera pensar en ella);)

Además, las mónadas proporcionan abstracciones muy generales y potentes , por lo que muchos tipos de flujo de control pueden ser convenientemente generalizados en funciones monádicas como sequence , liftM o syntax especial, lo que hace que la impureza no sea un caso tan especial.

Vea las mónadas en la progtwigción funcional y la tipificación de exclusividad (la única alternativa que conozco) para obtener más información.

Como dices, Monad es una estructura muy simple. La mitad de la respuesta es: Monad es la estructura más simple que podríamos dar a las funciones de efecto secundario y poder usarlas. Con Monad podemos hacer dos cosas: podemos tratar un valor puro como un valor de efecto secundario ( return ), y podemos aplicar una función de efecto lateral a un valor de efecto secundario para obtener un nuevo valor de efecto secundario ( >>= ). Perder la capacidad de hacer cualquiera de estas cosas sería paralizante, por lo que nuestro tipo de efectos secundarios debe ser “al menos” Monad , y resulta que Monad es suficiente para implementar todo lo que hemos necesitado hasta ahora.

La otra mitad es: ¿cuál es la estructura más detallada que podríamos dar a los “posibles efectos secundarios”? Ciertamente podemos pensar en el espacio de todos los efectos secundarios posibles como un conjunto (la única operación que requiere es la membresía). Podemos combinar dos efectos secundarios haciéndolos uno tras otro, y esto dará lugar a un efecto secundario diferente (o posiblemente el mismo – si el primero fue “apagar la computadora” y el segundo fue “escribir archivo”, entonces el resultado de componer estos es solo “computadora de apagado”).

Bien, entonces, ¿qué podemos decir sobre esta operación? Es asociativo; es decir, si combinamos tres efectos secundarios, no importa en qué orden hagamos la combinación. Si lo hacemos (escriba un archivo y luego lea el socket) luego apague la computadora, es lo mismo que hacer un archivo de escritura (leer el socket y luego apagarlo) computadora). Pero no es conmutativa: (“escribir archivo” luego “eliminar archivo”) es un efecto secundario diferente de (“eliminar archivo” luego “escribir archivo”). Y tenemos una identidad: el efecto secundario especial “sin efectos secundarios” funciona (“sin efectos secundarios”, luego “eliminar archivo” es el mismo efecto secundario que simplemente “eliminar archivo”). En este punto, cualquier matemático piensa “¡Grupo!” Pero los grupos tienen inversos, y no hay forma de invertir un efecto secundario en general; “borrar archivo” es irreversible. Entonces, la estructura que nos queda es la de un monoide, lo que significa que nuestras funciones de efecto secundario deben ser mónadas.

¿Hay una estructura más compleja? ¡Por supuesto! Podríamos dividir posibles efectos secundarios en efectos basados ​​en sistemas de archivos, efectos basados ​​en red y más, y podríamos elaborar reglas de composición más elaboradas que preservaran estos detalles. Pero, nuevamente, se trata de: Monad es muy simple y, sin embargo, lo suficientemente potente como para express la mayoría de las propiedades que nos interesan. (En particular, la asociatividad y los otros axiomas nos permiten probar nuestra aplicación en pequeños fragmentos, con la confianza de que los efectos secundarios de la aplicación combinada serán los mismos que la combinación de los efectos secundarios de las piezas).

En realidad, es una manera bastante limpia de pensar en E / S de una manera funcional.

En la mayoría de los lenguajes de progtwigción, realiza operaciones de entrada / salida. En Haskell, imagine escribir código para no hacer las operaciones, pero para generar una lista de las operaciones que le gustaría hacer.

Las mónadas son simplemente una syntax bonita para eso exactamente.

Si quieres saber por qué mónadas en lugar de otra cosa, supongo que la respuesta es que son la mejor forma funcional de representar E / S en las que la gente podría pensar cuando estaban haciendo Haskell.

AFAIK, el motivo es poder incluir controles de efectos secundarios en el sistema de tipos. Si quiere saber más, escuche esos episodios de SE-Radio : Episodio 108: Simon Peyton Jones en Progtwigción Funcional y Haskell Episodio 72: Erik Meijer en LINQ

Arriba hay muy buenas respuestas detalladas con antecedentes teóricos. Pero quiero dar mi opinión sobre la mónada IO. No soy un experimentado progtwigdor de Haskell, así que puede ser bastante ingenuo o incluso incorrecto. Pero me ayudó a lidiar con la mónada IO hasta cierto punto (tenga en cuenta que no se relaciona con otras mónadas).

Primero quiero decir que ese ejemplo con “mundo real” no es demasiado claro para mí, ya que no podemos acceder a sus estados previos (del mundo real). Puede ser que no se relacione con los cómputos de la mónada, pero se desea en el sentido de transparencia referencial, que generalmente se presenta en el código de Haskell.

Entonces queremos que nuestro lenguaje (haskell) sea puro. Pero necesitamos operaciones de entrada / salida ya que sin ellas nuestro progtwig no puede ser útil. Y esas operaciones no pueden ser puras por su naturaleza. Entonces, la única manera de lidiar con esto es separar las operaciones impuras del rest del código.

Aquí viene la mónada. En realidad, no estoy seguro de que no pueda existir otra construcción con propiedades necesarias similares, pero el punto es que la mónada tiene estas propiedades, por lo que puede usarse (y se usa con éxito). La propiedad principal es que no podemos escapar de ella. La interfaz Monad no tiene operaciones para deshacerse de la mónada alrededor de nuestro valor. Otras mónadas (no IO) proporcionan tales operaciones y permiten la coincidencia de patrones (por ejemplo, Maybe), pero esas operaciones no están en la interfaz de mónada. Otra propiedad requerida es la capacidad de encadenar operaciones.

Si pensamos en lo que necesitamos en términos de sistema de tipos, llegamos al hecho de que necesitamos el tipo con el constructor, que puede ajustarse a cualquier valor. El constructor debe ser privado, ya que prohibimos escapar de él (es decir, coincidencia de patrones). Pero necesitamos funciones para poner valor a este constructor (aquí viene a la mente el retorno). Y necesitamos la forma de encadenar las operaciones. Si lo pensamos durante algún tiempo, llegaremos al hecho de que la operación de encadenamiento debe tener el tipo >> >>. Entonces, llegamos a algo muy similar a la mónada. Creo que si ahora analizamos posibles situaciones contradictorias con este constructo, llegaremos a la mónada de axiomas.

Tenga en cuenta que esa construcción desarrollada no tiene nada en común con la impureza. Solo tiene propiedades que queríamos poder manejar con operaciones impuras, es decir, sin escape, encadenamiento y una forma de entrar.

Ahora, un conjunto de operaciones impuras está predefinido por el idioma dentro de esta mónada IO seleccionada. Podemos combinar esas operaciones para crear nuevas operaciones unpure. Y todas esas operaciones tendrán que tener IO en su tipo. Sin embargo, tenga en cuenta que la presencia de IO en el tipo de alguna función no hace que esta función sea impura. Pero, según tengo entendido, no es buena idea escribir funciones puras con IO en su tipo, ya que inicialmente era nuestra idea separar funciones puras e impuras.

Finalmente, quiero decir que esa mónada no convierte las operaciones impuras en operaciones puras. Solo permite separarlos de manera efectiva. (Repito, eso es solo mi entendimiento)

    Intereting Posts