Diferentes formas de ver una mónada

29

Mientras aprendía Haskell, me enfrenté a muchos tutoriales que intentaban explicar qué son las mónadas y por qué las mónadas son importantes en Haskell. Cada uno de ellos utilizó analogías, por lo que sería más fácil captar el significado. Al final del día, he terminado con 3 vistas diferentes de lo que es una mónada:

Vista 1: Mónada como etiqueta

A veces pienso que una mónada como etiqueta para un tipo específico. Por ejemplo, una función de tipo:

myfunction :: IO Int

myfunction es una función que cada vez que se realiza producirá un valor Int. El tipo de resultado no es Int sino IO Int. Entonces, IO es una etiqueta del valor Int que advierte al usuario que debe saber que el valor Int es el resultado de un proceso en el que se realizó una acción IO.

En consecuencia, este valor Int se ha marcado como valor que proviene de un proceso con IO, por lo tanto, este valor está "sucio". Tu proceso ya no es puro.

Vista 2: Monad como un espacio privado donde pueden suceder cosas desagradables.

En un sistema donde todo el proceso es puro y estricto, a veces es necesario tener efectos secundarios. Entonces, una mónada es solo un pequeño espacio que te permite hacer efectos secundarios desagradables. En este espacio, puedes escapar del mundo puro, volverse impuro, hacer tu proceso y luego volver con un valor.

Vista 3: Mónada como en teoría de categoría

Esta es la opinión que no entiendo completamente. Una mónada es solo un functor de la misma categoría o subcategoría. Por ejemplo, tiene los valores Int y como subcategoría IO Int, que son los valores Int generados después de un proceso IO.

¿Son correctas estas opiniones? ¿Cuál es más preciso?

Oni
fuente
55
# 2 no es lo que es una mónada en general. De hecho, está bastante restringido a IO, y no es una vista útil (cf. Qué no es una mónada ). Además, "estricto" generalmente se toma para nombrar una propiedad que Haskell no posee (es decir, evaluación estricta). Por cierto, las mónadas tampoco cambian eso (de nuevo, vea Qué no es una mónada).
3
Técnicamente, solo el tercero es correcto. Monad es endofunctor, para lo que se definen operaciones especiales: promoción y encuadernación. Las mónadas son numerosas: una mónada de lista es un ejemplo perfecto para tener intuición detrás de las mónadas. las instalaciones de readS son aún mejores. Sorprendentemente, las mónadas son utilizables como herramientas para enhebrar el estado en lenguaje funcional puro implícitamente. Esta no es una propiedad definitoria de las mónadas: es una coincidencia que el subproceso de estado se pueda implementar en sus términos. Lo mismo se aplica a IO.
permeakra
Common Lisp tiene su propio compilador como parte del lenguaje. Haskell tiene mónadas.
Will Ness

Respuestas:

33

Las vistas 1 y 2 son incorrectas en general.

  1. Cualquier tipo de datos * -> *puede funcionar como una etiqueta, las mónadas son mucho más que eso.
  2. (Con la excepción de la IOmónada) los cálculos dentro de una mónada no son impuros. Simplemente representan cálculos que percibimos que tienen efectos secundarios, pero son puros.

Ambos malentendidos provienen de centrarse en la IOmónada, que en realidad es un poco especial.

Intentaré elaborar un poco el # 3, sin entrar en la teoría de la categoría si es posible.


Cálculos estándar

Todos los cálculos en un lenguaje de programación funcional se pueden ver como funciona con un tipo de fuente y un tipo de destino: f :: a -> b. Si una función tiene más de un argumento, podemos convertirla en una función de un argumento al cursar (ver también wiki de Haskell ). Y si tenemos sólo un valor x :: a(una función con argumentos 0), podemos convertirlo en una función que toma un argumento del tipo de unidad : (\_ -> x) :: () -> a.

Podemos construir programas más complejos a partir de otros más simples componiendo tales funciones usando el .operador. Por ejemplo, si tenemos f :: a -> by g :: b -> cobtenemos g . f :: a -> c. Tenga en cuenta que esto también funciona para nuestros valores convertidos: si lo tenemos x :: ay lo convertimos en nuestra representación, obtenemos f . ((\_ -> x) :: () -> a) :: () -> b.

Esta representación tiene algunas propiedades muy importantes, a saber:

  • Tenemos una función muy especial: la función de identidadid :: a -> a para cada tipo a. Es un elemento de identidad con respecto a .: fes igual a f . idy a id . f.
  • El operador de composición de funciones .es asociativo .

Cálculos monádicos

Supongamos que queremos seleccionar y trabajar con alguna categoría especial de cálculos, cuyo resultado contiene algo más que el valor de retorno único. No queremos especificar qué significa "algo más", queremos mantener las cosas lo más generales posible. La forma más general de representar "algo más" es representarlo como una función de tipo, un tipo mde tipo * -> *(es decir, convierte un tipo en otro). Entonces, para cada categoría de cálculos con los que queremos trabajar, tendremos alguna función de tipo m :: * -> *. (En Haskell, mes [], IO, Maybe, etc.) y la categoría voluntad contiene todas las funciones de tipos a -> m b.

Ahora nos gustaría trabajar con las funciones en dicha categoría de la misma manera que en el caso básico. Queremos poder componer estas funciones, queremos que la composición sea asociativa y queremos tener una identidad. Necesitamos:

  • Tener un operador (llamémoslo <=<) que compone funciones f :: a -> m by g :: b -> m cen algo como g <=< f :: a -> m c. Y, debe ser asociativo.
  • Para tener alguna función de identidad para cada tipo, llamémosla return. También queremos que f <=< returnsea ​​lo mismo fy lo mismo que return <=< f.

Cualquiera m :: * -> *para el que tenemos tales funciones returny <=<se llama mónada . Nos permite crear cálculos complejos a partir de los más simples, como en el caso básico, pero ahora los tipos de valores de retorno se transforman por m.

(En realidad, abusé un poco del término categoría aquí. En el sentido de teoría de categorías, podemos llamar a nuestra construcción una categoría solo después de saber que obedece estas leyes).

Mónadas en Haskell

En Haskell (y otros lenguajes funcionales) trabajamos principalmente con valores, no con funciones de tipos () -> a. Entonces, en lugar de definir <=<para cada mónada, definimos una función (>>=) :: m a -> (a -> m b) -> m b. Dicha definición alternativa es equivalente, podemos expresarla >>=usando <=<y viceversa (intente como ejercicio o vea las fuentes ). El principio es menos obvio ahora, pero sigue siendo el mismo: nuestros resultados son siempre de tipos m ay componimos funciones de tipos a -> m b.

Para cada mónada que creamos, no debemos olvidar verificar eso returny <=<tener las propiedades que requerimos: asociatividad e identidad izquierda / derecha. Expresado usando returny >>=se llaman las leyes de mónada .

Un ejemplo: listas

Si elegimos mser [], obtenemos una categoría de funciones de tipos a -> [b]. Dichas funciones representan cálculos no deterministas, cuyos resultados podrían ser uno o más valores, pero tampoco valores. Esto da lugar a la llamada lista mónada . La composición f :: a -> [b]y g :: b -> [c]funciona de la siguiente manera: g <=< f :: a -> [c]significa calcular todos los resultados de tipo posibles [b], aplicarlos ga cada uno de ellos y recopilar todos los resultados en una sola lista. Expresado en Haskell

return :: a -> [a]
return x = [x]
(<=<) :: (b -> [c]) -> (a -> [b]) -> (a -> [c])
g (<=<) f  = concat . map g . f

o usando >>=

(>>=) :: [a] -> (a -> [b]) -> [b]
x >>= f  = concat (map f x)

Tenga en cuenta que, en este ejemplo, los tipos de retorno eran [a]tan posibles que no contenían ningún valor de tipo a. De hecho, no existe un requisito para una mónada de que el tipo de retorno debe tener tales valores. Algunas mónadas siempre tienen (me gusta IOo State), pero otras no, me gusta []o Maybe.

La mónada IO

Como mencioné, la IOmónada es algo especial. Un valor de tipo IO asignifica un valor de tipo aconstruido al interactuar con el entorno del programa. Entonces (a diferencia de todas las otras mónadas), no podemos describir un valor de tipo IO ausando alguna construcción pura. Aquí IOhay simplemente una etiqueta o una etiqueta que distingue los cálculos que interactúan con el entorno. Este es (el único caso) donde las vistas # 1 y # 2 son correctas.

Para la IOmónada:

  • Composición f :: a -> IO by g :: b -> IO cmedios: Computación fque interactúa con el entorno, y luego computación gque utiliza el valor y calcula el resultado interactuando con el entorno.
  • returnsimplemente agrega la IO"etiqueta" al valor (simplemente "calculamos" el resultado manteniendo el entorno intacto).
  • Las leyes de mónada (asociatividad, identidad) están garantizadas por el compilador.

Algunas notas:

  1. Dado que los cálculos monádicos siempre tienen el tipo de resultado m a, no hay forma de "escapar" de la IOmónada. El significado es: una vez que una computación interactúa con el entorno, no se puede construir una computación que no lo haga.
  2. Cuando un programador funcional no sabe cómo hacer algo de una manera pura, puede (como último recurso) programar la tarea mediante algún cálculo con estado dentro de la IOmónada. Esta es la razón por la cual a IOmenudo se le llama bin bin de programador .
  3. Tenga en cuenta que en un mundo impuro (en el sentido de la programación funcional) leer un valor también puede cambiar el entorno (como consumir la entrada del usuario). Es por eso que funciones como getChardeben tener un tipo de resultado de IO something.
Petr Pudlák
fuente
3
Gran respuesta. Debo aclarar que IOno tiene una semántica especial desde el punto de vista del lenguaje. Es no especial, se comporta como cualquier otro código. Solo la implementación de la biblioteca en tiempo de ejecución es especial. Además, hay una forma especial de escapar ( unsafePerformIO). Creo que esto es importante porque la gente suele pensar IOen un elemento de lenguaje especial o una etiqueta declarativa. No lo es.
usr
2
@usr Buen punto. Agregaría que unsafePerformIO es realmente inseguro y solo debe ser utilizado por expertos. Le permite romper todo, por ejemplo, puede crear una función coerce :: a -> bque convierta dos tipos (y bloquee su programa en la mayoría de los casos). Vea este ejemplo : puede convertir incluso una función en Intetc.
Petr Pudlák
Otra mónada de "magia especial" sería ST, que le permite declarar referencias a la memoria de las que puede leer y escribir como mejor le parezca (aunque solo dentro de la mónada), y luego puede extraer un resultado llamando arunST :: (forall s. GHC.ST.ST s a) -> a
sara
5

Vista 1: Mónada como etiqueta

"En consecuencia, este valor Int se ha marcado como valor que proviene de un proceso con IO, por lo tanto, este valor es" sucio "".

"IO Int" no es en general un valor Int (aunque puede ser en algunos casos como "return 3"). Es un procedimiento que genera algún valor Int. Las diferentes ejecuciones de este "procedimiento" pueden producir diferentes valores Int.

Una mónada m es un "lenguaje de programación" incrustado (imperativo): dentro de este lenguaje es posible definir algunos "procedimientos". Un valor monádico (de tipo ma) es un procedimiento en este "lenguaje de programación" que genera un valor de tipo a.

Por ejemplo:

foo :: IO Int

es un procedimiento que genera un valor de tipo Int.

Luego:

bar :: IO (Int, Int)
bar = do
  a <- foo
  b <- foo
  return (a,b)

es un procedimiento que genera dos (posiblemente diferentes) ent.

Cada "lenguaje" de este tipo admite algunas operaciones:

  • dos procedimientos (ma y mb) pueden "concatenarse": puede crear un procedimiento más grande (ma >> mb) compuesto por el primero y luego por el segundo;

  • además, la salida (a) del primero puede afectar al segundo (ma >> = \ a -> ...);

  • un procedimiento (retorno x) puede producir algún valor constante (x).

Los diferentes lenguajes de programación integrados difieren en las cosas amables que admiten, como:

  • produciendo valores aleatorios;
  • "bifurcación" (la [] mónada);
  • excepciones (lanzar / atrapar) (The Either e mónada);
  • continuación explícita / soporte de callcc;
  • enviar / recibir mensajes a otros "agentes";
  • crear, establecer y leer variables (locales para este lenguaje de programación) (la mónada ST).
ysdx
fuente
1

No confunda un tipo monádico con la clase mónada.

Un tipo monádico (es decir, un tipo que es una instancia de la clase mónada) resolvería un problema particular (en principio, cada tipo monádico resuelve uno diferente): Estado, Aleatorio, Quizás, IO. Todos ellos son tipos con contexto (lo que llamas "etiqueta", pero eso no es lo que los hace ser una mónada).

Para todos ellos, existe la necesidad de "encadenar operaciones con opción" (una operación depende del resultado de la anterior). Aquí entra en juego la clase mónada: haga que su tipo (resolviendo un problema dado) sea una instancia de la clase mónada y se resuelva el problema de encadenamiento.

Ver ¿Qué resuelve la clase mónada?

cibercitizen1
fuente