Mónada en inglés simple? (Para el programador OOP sin antecedentes de FP)


743

En términos que un programador de OOP entendería (sin ningún fondo de programación funcional), ¿qué es una mónada?

¿Qué problema resuelve y cuáles son los lugares más comunes en los que se usa?

EDITAR:

Para aclarar el tipo de comprensión que estaba buscando, digamos que estaba convirtiendo una aplicación FP que tenía mónadas en una aplicación OOP. ¿Qué harías para transferir las responsabilidades de las mónadas a la aplicación OOP?




10
@Pavel: La respuesta que tenemos a continuación de Eric es mucho mejor que la de las otras Q sugeridas para personas con antecedentes de OO (en oposición a un fondo de FP).
Donal Fellows

55
@Donal: Si esto es un engaño (sobre el cual no tengo opinión), la buena respuesta debería agregarse al original. Es decir: una buena respuesta no impide cerrar como duplicado. Si es un duplicado lo suficientemente cercano, esto puede ser realizado por un moderador como una fusión.
dmckee --- ex-gatito moderador

Respuestas:


732

ACTUALIZACIÓN: Esta pregunta fue el tema de una serie de blogs inmensamente larga, que puedes leer en Monads . ¡Gracias por la gran pregunta!

En términos que un programador de OOP entendería (sin ningún fondo de programación funcional), ¿qué es una mónada?

Una mónada es un "amplificador" de tipos que obedece ciertas reglas y que tiene ciertas operaciones proporcionadas .

Primero, ¿qué es un "amplificador de tipos"? Con eso me refiero a un sistema que le permite tomar un tipo y convertirlo en un tipo más especial. Por ejemplo, en C # considere Nullable<T>. Este es un amplificador de tipos. Le permite tomar un tipo, decir inty agregar una nueva capacidad a ese tipo, a saber, que ahora puede ser nulo cuando no podía antes.

Como segundo ejemplo, considérelo IEnumerable<T>. Es un amplificador de tipos. Le permite tomar un tipo, digamos, stringy agregar una nueva capacidad a ese tipo, a saber, que ahora puede hacer una secuencia de cadenas de cualquier número de cadenas individuales.

¿Cuáles son las "ciertas reglas"? Brevemente, que hay una manera sensata para que las funciones en el tipo subyacente trabajen en el tipo amplificado de manera que sigan las reglas normales de composición funcional. Por ejemplo, si tiene una función en enteros, diga

int M(int x) { return x + N(x * 2); }

entonces la función correspondiente en Nullable<int>puede hacer que todos los operadores y llamadas allí trabajen juntos "de la misma manera" que antes.

(Eso es increíblemente vago e impreciso; solicitó una explicación que no suponía nada sobre el conocimiento de la composición funcional).

¿Qué son las "operaciones"?

  1. Hay una operación de "unidad" (confusamente a veces llamada operación de "retorno") que toma un valor de un tipo simple y crea el valor monádico equivalente. Esto, en esencia, proporciona una manera de tomar un valor de un tipo no amplificado y convertirlo en un valor del tipo amplificado. Podría implementarse como un constructor en un lenguaje OO.

  2. Hay una operación de "vinculación" que toma un valor monádico y una función que puede transformar el valor, y devuelve un nuevo valor monádico. La vinculación es la operación clave que define la semántica de la mónada. Nos permite transformar operaciones en el tipo no amplificado en operaciones en el tipo amplificado, que obedece las reglas de composición funcional mencionadas anteriormente.

  3. A menudo hay una manera de recuperar el tipo no amplificado del tipo amplificado. Estrictamente hablando, esta operación no requiere tener una mónada. (Aunque es necesario si desea tener una comonad . No los consideraremos más en este artículo).

Nuevamente, tome Nullable<T>como ejemplo. Puedes convertir una inten una Nullable<int>con el constructor. El compilador de C # se encarga de la mayoría de los "levantamientos" anulables para usted, pero si no fuera así, la transformación de levantamiento es sencilla: una operación, por ejemplo,

int M(int x) { whatever }

se transforma en

Nullable<int> M(Nullable<int> x) 
{ 
    if (x == null) 
        return null; 
    else 
        return new Nullable<int>(whatever);
}

Y convertir una Nullable<int>copia de seguridad en una intse hace con la Valuepropiedad.

La transformación de la función es el bit clave. Observe cómo la semántica real de la operación anulable (que una operación en un nullpropaga null) se captura en la transformación. Podemos generalizar esto.

Supongamos que tiene una función de inta int, como nuestro original M. Puede convertirlo fácilmente en una función que tome un inty devuelva un Nullable<int>porque simplemente puede ejecutar el resultado a través del constructor anulable. Ahora suponga que tiene este método de orden superior:

static Nullable<T> Bind<T>(Nullable<T> amplified, Func<T, Nullable<T>> func)
{
    if (amplified == null) 
        return null;
    else
        return func(amplified.Value);
}

¿Ves lo que puedes hacer con eso? Cualquier método que tome un inty devuelva un int, o tome un inty devuelva un Nullable<int>puede ahora tener la semántica anulable aplicada .

Además: suponga que tiene dos métodos

Nullable<int> X(int q) { ... }
Nullable<int> Y(int r) { ... }

y quieres componerlos:

Nullable<int> Z(int s) { return X(Y(s)); }

Es decir, Zes la composición de Xy Y. Pero no puede hacerlo porque Xtoma un inty Ydevuelve un Nullable<int>. Pero como tiene la operación "vincular", puede hacer que esto funcione:

Nullable<int> Z(int s) { return Bind(Y(s), X); }

La operación de enlace en una mónada es lo que hace que la composición de funciones en tipos amplificados funcione. Las "reglas" que mencioné anteriormente son que la mónada preserva las reglas de la composición de la función normal; que componer con funciones de identidad da como resultado la función original, que la composición es asociativa, etc.

En C #, "Bind" se llama "SelectMany". Echa un vistazo a cómo funciona en la secuencia mónada. Necesitamos tener dos cosas: convertir un valor en una secuencia y vincular operaciones en secuencias. Como beneficio adicional, también tenemos "convertir una secuencia nuevamente en un valor". Esas operaciones son:

static IEnumerable<T> MakeSequence<T>(T item)
{
    yield return item;
}
// Extract a value
static T First<T>(IEnumerable<T> sequence)
{
    // let's just take the first one
    foreach(T item in sequence) return item; 
    throw new Exception("No first item");
}
// "Bind" is called "SelectMany"
static IEnumerable<T> SelectMany<T>(IEnumerable<T> seq, Func<T, IEnumerable<T>> func)
{
    foreach(T item in seq)
        foreach(T result in func(item))
            yield return result;            
}

La regla de mónada anulable era "combinar dos funciones que produzcan valores anulables juntos, verifique si el interno da como resultado nulo; si lo hace, produzca nulo, si no es así, llame al externo con el resultado". Esa es la semántica deseada de anulable.

La regla de la mónada de secuencia es "combinar dos funciones que producen secuencias juntas, aplicar la función externa a cada elemento producido por la función interna y luego concatenar todas las secuencias resultantes juntas". La semántica fundamental de las mónadas se captura en los métodos Bind/ SelectMany; Este es el método que te dice lo que realmente significa la mónada .

Podemos hacerlo aún mejor. Suponga que tiene una secuencia de entradas y un método que toma entradas y da como resultado secuencias de cadenas. Podríamos generalizar la operación de enlace para permitir la composición de funciones que toman y devuelven diferentes tipos amplificados, siempre que las entradas de una coincidan con las salidas de la otra:

static IEnumerable<U> SelectMany<T,U>(IEnumerable<T> seq, Func<T, IEnumerable<U>> func)
{
    foreach(T item in seq)
        foreach(U result in func(item))
            yield return result;            
}

Así que ahora podemos decir "amplifique este grupo de enteros individuales en una secuencia de números enteros. Transforme este número entero particular en un grupo de cadenas, amplificado en una secuencia de cadenas. Ahora junte ambas operaciones: amplifique este grupo de números enteros en la concatenación de todas las secuencias de cuerdas ". Las mónadas te permiten componer tus amplificaciones.

¿Qué problema resuelve y cuáles son los lugares más comunes en los que se usa?

Es como preguntar "¿qué problemas resuelve el patrón singleton?", Pero lo intentaré.

Las mónadas se suelen utilizar para resolver problemas como:

  • Necesito crear nuevas capacidades para este tipo y aun así combinar funciones antiguas en este tipo para usar las nuevas capacidades.
  • Necesito capturar un montón de operaciones en tipos y representar esas operaciones como objetos componibles, construyendo composiciones cada vez más grandes hasta que tenga representada la serie correcta de operaciones, y luego necesito comenzar a obtener resultados.
  • Necesito representar las operaciones de efectos secundarios limpiamente en un lenguaje que odie los efectos secundarios.

C # utiliza mónadas en su diseño. Como ya se mencionó, el patrón anulable es muy similar a la "quizás mónada". LINQ está completamente construido de mónadas; El SelectManymétodo es lo que hace el trabajo semántico de composición de operaciones. (Erik Meijer es aficionado a señalar que todas las funciones de LINQ podrían implementarse realmente SelectMany; todo lo demás es solo una conveniencia).

Para aclarar el tipo de comprensión que estaba buscando, digamos que estaba convirtiendo una aplicación FP que tenía mónadas en una aplicación OOP. ¿Qué harías para transferir las responsabilidades de las mónadas a la aplicación OOP?

La mayoría de los lenguajes OOP no tienen un sistema de tipos lo suficientemente rico como para representar el patrón de mónada directamente; necesita un sistema de tipos que admita tipos superiores a los genéricos. Entonces no trataría de hacer eso. Más bien, implementaría tipos genéricos que representan cada mónada e implementaría métodos que representan las tres operaciones que necesita: convertir un valor en un valor amplificado, (tal vez) convertir un valor amplificado en un valor y transformar una función en valores no amplificados en una función en valores amplificados.

Un buen lugar para comenzar es cómo implementamos LINQ en C #. Estudiar el SelectManymétodo; es la clave para entender cómo funciona la secuencia mónada en C #. ¡Es un método muy simple, pero muy poderoso!


Sugerencia, lectura adicional:

  1. Para una explicación más profunda y teóricamente sólida de las mónadas en C #, recomiendo mucho el artículo de mi colega ( Eric Lippert ) Wes Dyer sobre el tema. Este artículo es lo que me explicó las mónadas cuando finalmente "hicieron clic" para mí.
  2. Una buena ilustración de por qué es posible que desee una mónada (utiliza Haskell en sus ejemplos) .
  3. Una especie de "traducción" del artículo anterior a JavaScript.


17
Esta es una gran respuesta, pero mi cabeza se puso nerviosa. Haré un seguimiento y lo miraré este fin de semana y te haré preguntas si las cosas no se calman y tienen sentido en mi cabeza.
Paul Nathan

55
Excelente explicación como siempre Eric. Para una discusión más teórica (pero aún muy interesante), he encontrado útil la publicación de blog de Bart De Smet sobre MinLINQ para relacionar algunas construcciones de programación funcional también en C #. community.bartdesmet.net/blogs/bart/archive/2010/01/01/…
Ron Warholic

41
Para mí tiene más sentido decir que aumenta los tipos en lugar de amplificarlos .
Gabe

61
@slomojo: y lo cambié de nuevo a lo que escribí y tenía la intención de escribir. Si usted y Gabe quieren escribir su propia respuesta, adelante.
Eric Lippert

24
@Eric, depende de usted, por supuesto, pero Amplificador implica que las propiedades existentes se incrementan, lo que es engañoso.
ocodo

341

¿Por qué necesitamos mónadas?

  1. Queremos programar solo usando funciones . ("programación funcional" después de todo -FP).
  2. Entonces, tenemos un primer gran problema. Este es un programa:

    f(x) = 2 * x

    g(x,y) = x / y

    ¿Cómo podemos decir qué se debe ejecutar primero ? ¿Cómo podemos formar una secuencia ordenada de funciones (es decir, un programa ) usando no más que funciones?

    Solución: componer funciones . Si quieres primero gy luego f, solo escribe f(g(x,y)). Bien pero ...

  3. Más problemas: algunas funciones pueden fallar (es decir g(2,0), dividir por 0). No tenemos "excepciones" en FP . ¿Como lo resolvemos?

    Solución: Permitamos que las funciones devuelvan dos tipos de cosas : en lugar de tener g : Real,Real -> Real(función de dos reales en real), permitamos g : Real,Real -> Real | Nothing(función de dos reales en (real o nada)).

  4. Pero las funciones deberían (para ser más simples) devolver solo una cosa .

    Solución: creemos un nuevo tipo de datos para devolver, un " tipo de boxeo " que encierra quizás un real o simplemente nada. Por lo tanto, podemos tener g : Real,Real -> Maybe Real. Bien pero ...

  5. ¿Qué pasa ahora con f(g(x,y))? fno está listo para consumir a Maybe Real. Y no queremos cambiar todas las funciones con las que podríamos conectarnos gpara consumir a Maybe Real.

    Solución: tengamos una función especial para "conectar" / "componer" / "vincular" funciones . De esa manera, podemos, detrás de escena, adaptar la salida de una función para alimentar la siguiente.

    En nuestro caso: g >>= f(conectar / componer ga f). Queremos >>=obtener gla salida, inspeccionarla y, en caso de que sea Nothing, no llame fy regrese Nothing; o por el contrario, extraiga el cuadro Realy alimente fcon él. (Este algoritmo es solo la implementación de >>=para el Maybetipo).

  6. Surgen muchos otros problemas que pueden resolverse usando este mismo patrón: 1. Use una "caja" para codificar / almacenar diferentes significados / valores, y tener funciones como gesa devolver esos "valores en caja". 2. Tenga compositores / enlazadores g >>= fpara ayudar a conectar gla salida de fla entrada, de modo que no tengamos que cambiar fen absoluto.

  7. Los problemas notables que se pueden resolver con esta técnica son:

    • teniendo un estado global que cada función en la secuencia de funciones ("el programa") puede compartir: solución StateMonad.

    • No nos gustan las "funciones impuras": funciones que producen diferentes resultados para la misma entrada. Por lo tanto, marquemos esas funciones, haciendo que devuelvan un valor etiquetado / encuadrado: IOmónada.

Felicidad total !!!!


2
@DmitriZaitsev Las excepciones solo pueden ocurrir en "código impuro" (la mónada IO) hasta donde yo sé.
cibercitizen1

3
@DmitriZaitsev El papel de Nothing puede ser jugado por cualquier otro tipo (diferente del real esperado). Ese no es el punto. En el ejemplo, la cuestión es cómo adaptar funciones en una cadena cuando la anterior puede devolver un tipo de valor inesperado a la siguiente, sin encadenar la última (solo aceptando un Real como entrada).
cibercitizen1

3
Otro punto de confusión es que la palabra "mónada" aparece solo dos veces en su respuesta, y solo en combinación con otros términos, Statey IOsin darles ninguno de ellos ni el significado exacto de "mónada".
Dmitri Zaitsev

31
Para mí, como persona proveniente de un entorno de OOP, esta respuesta realmente explicó bien la motivación detrás de tener una mónada y también qué es realmente la mónada (mucho más que una respuesta aceptada). Entonces, me parece muy útil. Muchas gracias @ cibercitizen1 y +1
akhilless

3
He estado leyendo sobre programación funcional de vez en cuando durante aproximadamente un año. Esta respuesta, y especialmente los primeros dos puntos, finalmente me hicieron comprender lo que realmente significa la programación imperativa y por qué la programación funcional es diferente. ¡Gracias!
jrahhali

82

Yo diría que la analogía OO más cercana a las mónadas es el " patrón de comando ".

En el patrón de comando, envuelve una declaración o expresión ordinaria en un objeto de comando . El objeto de comando expone un método de ejecución que ejecuta la instrucción envuelta. Por lo tanto, las declaraciones se convierten en objetos de primera clase que se pueden pasar y ejecutar a voluntad. Los comandos se pueden componer para que pueda crear un objeto de programa encadenando y anidando objetos de comando.

Los comandos son ejecutados por un objeto separado, el invocador . El beneficio de usar el patrón de comando (en lugar de simplemente ejecutar una serie de declaraciones ordinarias) es que diferentes invocadores pueden aplicar una lógica diferente a cómo deben ejecutarse los comandos.

El patrón de comando podría usarse para agregar (o eliminar) funciones de idioma que no es compatible con el idioma del host. Por ejemplo, en un lenguaje OO hipotético sin excepciones, puede agregar una semántica de excepción al exponer los métodos "probar" y "lanzar" a los comandos. Cuando un comando llama lanzar, el invocador retrocede a través de la lista (o árbol) de comandos hasta la última llamada de "intento". Por el contrario, puede eliminar la excepción semántica de un idioma (si cree que las excepciones son malas ) capturando todas las excepciones lanzadas por cada comando individual y convirtiéndolas en códigos de error que luego se pasan al siguiente comando.

Incluso se pueden implementar semánticas de ejecución más sofisticadas como transacciones, ejecuciones no deterministas o continuaciones de esta manera en un lenguaje que no lo admite de forma nativa. Es un patrón bastante poderoso si lo piensas.

Ahora, en realidad, los patrones de comando no se usan como una característica de lenguaje general como esta. La sobrecarga de convertir cada declaración en una clase separada conduciría a una cantidad insoportable de código repetitivo. Pero, en principio, se puede usar para resolver los mismos problemas que las mónadas se usan para resolver en fp.


15
Creo que esta es la primera explicación de mónada que he visto que no se basó en conceptos de programación funcional y lo puso en términos reales de OOP. Muy buena respuesta.
David K. Hess

esto está muy cerca de lo que realmente son las mónadas en FP / Haskell, excepto que los objetos de comando "saben" a qué "lógica de invocación" pertenecen (y solo los compatibles pueden encadenarse juntos); invocador solo proporciona el primer valor. No es que el comando "Imprimir" pueda ejecutarse mediante una "lógica de ejecución no determinista". No, tiene que ser "lógica de E / S" (es decir, IO monad). Pero aparte de eso, está muy cerca. Incluso podría decir que las mónadas son solo programas (construidos con declaraciones de código, que se ejecutarán más adelante). En los primeros días se hablaba de "enlace" como "punto y coma programable" .
Will Ness

1
@ DavidK.Hess Soy realmente escéptico de las respuestas que usan FP para explicar los conceptos básicos de FP, y especialmente las respuestas que usan un lenguaje de FP como Scala. ¡Bien hecho, JacquesB!
Vuelva a instalar Mónica

62

En términos que un programador de OOP entendería (sin ningún fondo de programación funcional), ¿qué es una mónada?

¿Qué problema resuelve y cuáles son los lugares más comunes en los que se usa? ¿Cuáles son los lugares más comunes en los que se usa?

En términos de programación OO, una mónada es una interfaz (o más probablemente un mixin), parametrizada por un tipo, con dos métodos, returny bindque describe:

  • Cómo inyectar un valor para obtener un valor monádico de ese tipo de valor inyectado;
  • Cómo usar una función que crea un valor monádico de uno no monádico, en un valor monádico.

El problema que resuelve es el mismo tipo de problema que esperaría de cualquier interfaz, a saber, "Tengo un montón de clases diferentes que hacen cosas diferentes, pero parecen hacer esas cosas diferentes de una manera que tiene una similitud subyacente". ¿puedo describir esa similitud entre ellos, incluso si las clases en sí mismas no son realmente subtipos de algo más cercano que la clase 'el Objeto' en sí misma? "

Más específicamente, la Monad"interfaz" es similar IEnumeratoro IIteratoren que toma un tipo que en sí toma un tipo. Sin Monadembargo, el "punto" principal es poder conectar operaciones basadas en el tipo de interior, incluso hasta el punto de tener un nuevo "tipo interno", manteniendo - o incluso mejorando - la estructura de información de la clase principal.


1
returnen realidad no sería un método en la mónada, porque no toma una instancia de mónada como argumento. (es decir, no hay esto / yo mismo)
Laurence Gonsalves

@LaurenceGonsalves: dado que actualmente estoy investigando esto para mi tesis de licenciatura, creo que lo que más limita es la falta de métodos estáticos en las interfaces en C # / Java. Podría llegar lejos en la dirección de implementar toda la historia de la mónada, al menos estáticamente vinculada en lugar de basarse en clases de tipos. Curiosamente, esto incluso funcionaría a pesar de la falta de tipos de clase superior.
Sebastian Graf

42

Tiene una presentación reciente " Monadologie - ayuda profesional sobre la ansiedad de tipo " por Christopher League (12 de julio de 2010), que es bastante interesante sobre temas de continuación y mónada.
El video que acompaña esta presentación (slideshare) está realmente disponible en vimeo .
La parte de Monad comienza alrededor de 37 minutos, en este video de una hora, y comienza con la diapositiva 42 de su presentación de 58 diapositivas.

Se presenta como "el patrón de diseño líder para la programación funcional", pero el lenguaje utilizado en los ejemplos es Scala, que es tanto OOP como funcional.
Puede leer más sobre Mónada en Scala en la publicación del blog " Mónadas: otra forma de hacer cálculos abstractos en Scala ", de Debasish Ghosh (27 de marzo de 2008).

Un constructor de tipo M es una mónada si admite estas operaciones:

# the return function
def unit[A] (x: A): M[A]

# called "bind" in Haskell 
def flatMap[A,B] (m: M[A]) (f: A => M[B]): M[B]

# Other two can be written in term of the first two:

def map[A,B] (m: M[A]) (f: A => B): M[B] =
  flatMap(m){ x => unit(f(x)) }

def andThen[A,B] (ma: M[A]) (mb: M[B]): M[B] =
  flatMap(ma){ x => mb }

Entonces, por ejemplo (en Scala):

  • Option es una mónada
    unidad def [A] (x: A): Opción [A] = Some (x)

    def flatMap [A, B] (m: Opción [A]) (f: A => Opción [B]): Opción [B] =
      m partido {
       caso Ninguno => Ninguno
       caso Some (x) => f (x)
      }
  • List es mónada
    unidad def [A] (x: A): Lista [A] = Lista (x)

    def flatMap [A, B] (m: Lista [A]) (f: A => Lista [B]): Lista [B] =
      m partido {
        caso Nil => Nil
        case x :: xs => f (x) ::: flatMap (xs) (f)
      }

Monad es un gran problema en Scala debido a la conveniente sintaxis creada para aprovechar las estructuras de Monad:

forcomprensión en Scala :

for {
  i <- 1 to 4
  j <- 1 to i
  k <- 1 to j
} yield i*j*k

es traducido por el compilador a:

(1 to 4).flatMap { i =>
  (1 to i).flatMap { j =>
    (1 to j).map { k =>
      i*j*k }}}

La abstracción clave es el flatMap, que une el cálculo a través del encadenamiento.
Cada invocación de flatMapdevuelve el mismo tipo de estructura de datos (pero de diferente valor), que sirve como entrada para el siguiente comando en cadena.

En el fragmento anterior, flatMap toma como entrada un cierre (SomeType) => List[AnotherType]y devuelve a List[AnotherType]. El punto importante a tener en cuenta es que todos los flatMaps toman el mismo tipo de cierre como entrada y devuelven el mismo tipo como salida.

Esto es lo que "une" el hilo de cálculo: cada elemento de la secuencia en la comprensión tiene que cumplir con esta misma restricción de tipo.


Si realiza dos operaciones (que pueden fallar) y pasa el resultado a la tercera, como:

lookupVenue: String => Option[Venue]
getLoggedInUser: SessionID => Option[User]
reserveTable: (Venue, User) => Option[ConfNo]

pero sin aprovechar Monad, obtienes un código OOP complicado como:

val user = getLoggedInUser(session)
val confirm =
  if(!user.isDefined) None
  else lookupVenue(name) match {
    case None => None
    case Some(venue) =>
      val confno = reserveTable(venue, user.get)
      if(confno.isDefined)
        mailTo(confno.get, user.get)
      confno
  }

mientras que con Monad, puede trabajar con los tipos reales ( Venue, User) como todas las operaciones funcionan, y mantener ocultas las cosas de verificación de Opciones, todo debido a los planos de la sintaxis for:

val confirm = for {
  venue <- lookupVenue(name)
  user <- getLoggedInUser(session)
  confno <- reserveTable(venue, user)
} yield {
  mailTo(confno, user)
  confno
}

La parte de rendimiento solo se ejecutará si las tres funciones tienen Some[X]; cualquiera Nonesería devuelto directamente a confirm.


Entonces:

Las mónadas permiten el cálculo ordenado dentro de la programación funcional, que nos permite modelar la secuencia de acciones en una forma estructurada agradable, algo así como un DSL.

Y el mayor poder viene con la capacidad de componer mónadas que sirven para diferentes propósitos, en abstracciones extensibles dentro de una aplicación.

Esta secuencia y secuencia de acciones de una mónada la realiza el compilador de lenguaje que realiza la transformación a través de la magia de los cierres.


Por cierto, Monad no es solo un modelo de cálculo utilizado en FP:

La teoría de categorías propone muchos modelos de computación. Entre ellos

  • el modelo de flecha de los cálculos
  • El modelo de cálculos de Monad
  • El modelo aplicativo de cómputos

2
¡Me encanta esta explicación! El ejemplo que diste demuestra el concepto maravillosamente y también agrega lo que faltaba en mi humilde opinión del teaser de Eric sobre SelectMany () siendo una mónada. Gracias por esto!
aoven

1
En mi humilde opinión, esta es la respuesta más elegante
Polimerasa

y antes que nada, Functor.
Will Ness

34

Para respetar a los lectores rápidos, primero comienzo con una definición precisa, continúo con una explicación más rápida y sencilla, y luego paso a ejemplos.

Aquí hay una definición concisa y precisa ligeramente reformulada:

Una mónada (en informática) es formalmente un mapa que:

  • envía cada tipo Xde algún lenguaje de programación dado a un nuevo tipo T(X)(llamado "tipo de T-computaciones con valores en X");

  • equipado con una regla para componer dos funciones de la forma f:X->T(Y)y g:Y->T(Z)para una función g∘f:X->T(Z);

  • de una manera asociativa en el sentido evidente y unital con respecto a una función de unidad dada llamada pure_X:X->T(X), que puede considerarse como tomar un valor a la computación pura que simplemente devuelve ese valor.

Entonces, en palabras simples, una mónada es una regla para pasar de cualquier tipo Xa otro tipoT(X) , y una regla para pasar de dos funciones f:X->T(Y)y g:Y->T(Z)(que le gustaría componer pero no puede) a una nueva funciónh:X->T(Z) . Que, sin embargo, no es la composición en sentido matemático estricto. Básicamente estamos "doblando" la composición de la función o redefiniendo cómo se componen las funciones.

Además, requerimos que la regla de composición de la mónada satisfaga los axiomas matemáticos "obvios":

  • Asociatividad : componer fcon gy luego con h(desde afuera) debería ser lo mismo que componer gcon hy luego con f(desde adentro).
  • Propiedad unital : componerf con la función de identidad en cualquier lado debería rendir f.

Nuevamente, en palabras simples, no podemos volvernos locos redefiniendo nuestra composición de funciones como nos gusta:

  • Primero necesitamos la asociatividad para poder componer varias funciones en una fila f(g(h(k(x))), por ejemplo , y no tener que preocuparnos de especificar el orden que compone los pares de funciones. Como la regla de la mónada solo prescribe cómo componer un par de funciones , sin ese axioma, necesitaríamos saber qué par se compone primero y así sucesivamente. (Tenga en cuenta que es diferente de la propiedad de conmutatividad que fcompuso con geran las mismas que gcompuestas con f, lo cual no es obligatorio).
  • Y segundo, necesitamos la propiedad unital, que es simplemente decir que las identidades se componen trivialmente de la forma en que las esperamos. Por lo tanto, podemos refactorizar las funciones de forma segura siempre que se puedan extraer esas identidades.

En resumen, una mónada es la regla de extensión de tipo y funciones de composición que satisfacen los dos axiomas: asociatividad y propiedad unital.

En términos prácticos, desea que la mónada sea implementada por el lenguaje, el compilador o el marco que se encargaría de componer las funciones por usted. Por lo tanto, puede concentrarse en escribir la lógica de su función en lugar de preocuparse de cómo se implementa su ejecución.

Eso es esencialmente eso, en pocas palabras.


Siendo matemático profesional, prefiero evitar llamar a hla "composición" de fy g. Porque matemáticamente, no lo es. Llamarlo "composición" supone incorrectamente que hes la verdadera composición matemática, que no lo es. Ni siquiera está determinado únicamente por fy g. En cambio, es el resultado de la nueva "regla de componer" las funciones de nuestra mónada. ¡Lo que puede ser totalmente diferente de la composición matemática real, incluso si existe esta última!


Para hacerlo menos seco, permítame intentar ilustrarlo con un ejemplo que estoy anotando con secciones pequeñas, para que pueda saltar directamente al punto.

Lanzamiento de excepciones como ejemplos de Monad

Supongamos que queremos componer dos funciones:

f: x -> 1 / x
g: y -> 2 * y

Pero f(0)no está definido, por lo que ese lanza una excepción . Entonces, ¿cómo puedes definir el valor compositivo g(f(0))? ¡Lanza una excepción de nuevo, por supuesto! Quizás lo mismo e. Quizás una nueva excepción actualizada e1.

¿Qué sucede precisamente aquí? Primero, necesitamos nuevos valores de excepción (diferentes o iguales). Se les puede llamar nothingo nullo lo que sea, pero la esencia sigue siendo la misma - que deben ser valores nuevos, por ejemplo, que no debe ser una numberen nuestro ejemplo aquí. Prefiero no llamarlos nullpara evitar confusiones sobre cómo nullse puede implementar en un idioma específico. Igualmente, prefiero evitarlo nothingporque a menudo está asociado con lo nullque, en principio, es lo que nulldebería hacer, sin embargo, ese principio a menudo se dobla por cualquier razón práctica.

¿Qué es la excepción exactamente?

Este es un asunto trivial para cualquier programador experimentado, pero me gustaría dejar caer algunas palabras solo para extinguir cualquier gusano de confusión:

La excepción es un objeto que encapsula información sobre cómo se produjo el resultado no válido de la ejecución.

Esto puede variar desde descartar cualquier detalle y devolver un único valor global (como NaNo null) o generar una larga lista de registro o lo que sucedió exactamente, enviarlo a una base de datos y replicarlo en toda la capa de almacenamiento de datos distribuidos;)

La diferencia importante entre estos dos ejemplos extremos de excepción es que en el primer caso no hay efectos secundarios . En el segundo hay. Lo que nos lleva a la pregunta (de mil dólares):

¿Se permiten excepciones en funciones puras?

Respuesta más corta : Sí, pero solo cuando no conducen a efectos secundarios.

Respuesta larga Para ser puro, la salida de su función debe estar determinada únicamente por su entrada. Entonces modificamos nuestra función fenviando 0al nuevo valor abstracto eque llamamos excepción. Nos aseguramos de que el valor eno contenga información externa que no esté determinada únicamente por nuestra entrada, que es x. Así que aquí hay un ejemplo de excepción sin efectos secundarios:

e = {
  type: error, 
  message: 'I got error trying to divide 1 by 0'
}

Y aquí hay uno con efectos secundarios:

e = {
  type: error, 
  message: 'Our committee to decide what is 1/0 is currently away'
}

En realidad, solo tiene efectos secundarios si ese mensaje puede cambiar en el futuro. Pero si se garantiza que nunca cambiará, ese valor se vuelve predecible de manera única, por lo que no hay efectos secundarios.

Para hacerlo aún más tonto. Una función que vuelve 42siempre es claramente pura. Pero si alguien loco decide hacer 42una variable cuyo valor podría cambiar, la misma función deja de ser pura en las nuevas condiciones.

Tenga en cuenta que estoy usando la notación literal del objeto por simplicidad para demostrar la esencia. Desafortunadamente, las cosas están desordenadas en lenguajes como JavaScript, donde errorno es un tipo que se comporta de la manera que queremos aquí con respecto a la composición de funciones, mientras que los tipos reales les gusta nullo NaNno se comportan de esta manera, sino que pasan por algo artificial y no siempre intuitivo tipo de conversiones.

Extensión de tipo

Como queremos variar el mensaje dentro de nuestra excepción, realmente estamos declarando un nuevo tipo Epara todo el objeto de excepción y luego Eso es lo que maybe numberhace, aparte de su nombre confuso, que debe ser de tipo numbero del nuevo tipo de excepción E, entonces es realmente la unión number | Ede numbery E. En particular, depende de cómo queremos construir E, que no se sugiere ni se refleja en el nombre maybe number.

¿Qué es la composición funcional?

Se trata de las funciones matemáticas toma de la operación f: X -> Yy g: Y -> Z, y la construcción de su composición como función de h: X -> Zla satisfacción h(x) = g(f(x)). El problema con esta definición ocurre cuando el resultado f(x)no está permitido como argumento de g.

En matemáticas, esas funciones no se pueden componer sin un trabajo adicional. La solución estrictamente matemática para nuestro ejemplo anterior de fy ges eliminar 0del conjunto de definición de f. Con ese nuevo conjunto de definición (nuevo tipo más restrictivo x), fse puede componer con g.

Sin embargo, no es muy práctico en la programación restringir el conjunto de definiciones de fese tipo. En cambio, se pueden usar excepciones.

O como otro enfoque, los valores artificiales se crean como NaN, undefined, null, Infinityetc Por lo tanto se evalúa 1/0a Infinityy 1/-0a -Infinity. Y luego fuerce el nuevo valor nuevamente en su expresión en lugar de lanzar una excepción. Llevar a resultados que puede o no encontrar predecibles:

1/0                // => Infinity
parseInt(Infinity) // => NaN
NaN < 0            // => false
false + 1          // => 1

Y volvemos a los números regulares listos para seguir adelante;)

JavaScript nos permite seguir ejecutando expresiones numéricas a cualquier costo sin arrojar errores como en el ejemplo anterior. Eso significa que también permite componer funciones. De eso se trata exactamente la mónada: es una regla componer funciones que satisfagan los axiomas definidos al comienzo de esta respuesta.

Pero, ¿la regla de la función de composición, que surge de la implementación de JavaScript para tratar errores numéricos, es una mónada?

Para responder a esta pregunta, todo lo que necesita es verificar los axiomas (se deja como ejercicio como parte de la pregunta aquí;).

¿Se puede usar la excepción de lanzamiento para construir una mónada?

De hecho, una mónada más útil sería la regla que prescribe que si farroja una excepción para algunos x, también lo hace su composición con cualquiera g. Además, haga que la excepción sea Eglobalmente única con un solo valor posible ( objeto terminal en la teoría de categorías). Ahora los dos axiomas son comprobables al instante y obtenemos una mónada muy útil. Y el resultado es lo que se conoce como quizás la mónada .


3
Buen aporte. +1 Pero tal vez quieras eliminar "la mayoría de las explicaciones han sido encontradas por mucho tiempo ..." siendo la tuya la más larga. Otros juzgarán si es "inglés simple" como se requiere la pregunta: "inglés simple == en palabras simples, de una manera simple".
cibercitizen1

@ cibercitizen1 ¡Gracias! En realidad es corto, si no cuenta el ejemplo. El punto principal es que no necesita leer el ejemplo para comprender la definición . Desafortunadamente, muchas explicaciones me obligan a leer ejemplos primero , lo que a menudo es innecesario pero, por supuesto, puede requerir un trabajo adicional para el escritor. Con demasiada dependencia de ejemplos específicos, existe el peligro de que detalles sin importancia oscurezcan la imagen y dificulten su comprensión. Dicho esto, tienes puntos válidos, mira la actualización.
Dmitri Zaitsev

2
demasiado largo y confuso
seenimurugan

1
@seenimurugan Las sugerencias de mejora son bienvenidas;)
Dmitri Zaitsev

26

Una mónada es un tipo de datos que encapsula un valor y al que, esencialmente, se pueden aplicar dos operaciones:

  • return x crea un valor del tipo mónada que encapsula x
  • m >>= f(léalo como "el operador de enlace") aplica la función fal valor en la mónadam

Eso es una mónada. Hay algunos tecnicismos más , pero básicamente esas dos operaciones definen una mónada. La verdadera pregunta es: "¿Qué una mónada hace ?", Y que depende de la mónada - listas son mónadas, Maybes son mónadas, las operaciones de IO son mónadas. Todo lo que significa cuando decimos que esas cosas son mónadas es que tienen la interfaz de mónada returny >>=.


"Lo que hace una mónada, y eso depende de la mónada": y más precisamente, eso depende de la bindfunción que debe definirse para cada tipo de mónada, ¿no es así? Esa sería una buena razón para no confundir el enlace con la composición, ya que existe una única definición para la composición, mientras que no puede haber una sola definición para una función de enlace, hay una por tipo monádico, si lo entiendo correctamente.
Hibou57

14

De wikipedia :

En la programación funcional, una mónada es un tipo de datos abstractos que se utilizan para representar cálculos (en lugar de datos en el modelo de dominio). Las mónadas permiten al programador encadenar acciones para construir una tubería, en la que cada acción está decorada con reglas de procesamiento adicionales proporcionadas por la mónada. Los programas escritos en estilo funcional pueden usar mónadas para estructurar procedimientos que incluyen operaciones secuenciadas, 1 [2] o para definir flujos de control arbitrarios (como el manejo de concurrencia, continuaciones o excepciones).

Formalmente, una mónada se construye definiendo dos operaciones (vinculación y retorno) y un constructor de tipo M que debe cumplir varias propiedades para permitir la composición correcta de las funciones monádicas (es decir, funciones que usan valores de la mónada como sus argumentos). La operación de retorno toma un valor de un tipo simple y lo coloca en un contenedor monádico de tipo M. La operación de enlace realiza el proceso inverso, extrae el valor original del contenedor y lo pasa a la siguiente función asociada en la tubería.

Un programador compondrá funciones monádicas para definir una tubería de procesamiento de datos. La mónada actúa como un marco, ya que es un comportamiento reutilizable que decide el orden en que se llaman las funciones monádicas específicas en la tubería y administra todo el trabajo encubierto requerido por el cálculo. [3] Los operadores de enlace y retorno intercalados en la tubería se ejecutarán después de que cada función monádica devuelva el control, y se ocuparán de los aspectos particulares manejados por la mónada.

Creo que lo explica muy bien.


12

Trataré de hacer la definición más corta que pueda manejar usando términos OOP:

Una clase genérica CMonadic<T>es una mónada si define al menos los siguientes métodos:

class CMonadic<T> { 
    static CMonadic<T> create(T t);  // a.k.a., "return" in Haskell
    public CMonadic<U> flatMap<U>(Func<T, CMonadic<U>> f); // a.k.a. "bind" in Haskell
}

y si las siguientes leyes se aplican a todos los tipos T y sus posibles valores t

identidad izquierda:

CMonadic<T>.create(t).flatMap(f) == f(t)

identidad correcta

instance.flatMap(CMonadic<T>.create) == instance

asociatividad:

instance.flatMap(f).flatMap(g) == instance.flatMap(t => f(t).flatMap(g))

Ejemplos :

Una Mónada de Lista puede tener:

List<int>.create(1) --> [1]

Y flatMap en la lista [1,2,3] podría funcionar así:

intList.flatMap(x => List<int>.makeFromTwoItems(x, x*10)) --> [1,10,2,20,3,30]

Iterables y Observables también se pueden hacer monádicos, así como Promesas y Tareas.

comentario :

Las mónadas no son tan complicadas. La flatMapfunción es muy parecida a la más común map. Recibe un argumento de función (también conocido como delegado), al que puede llamar (inmediatamente o más tarde, cero o más veces) con un valor proveniente de la clase genérica. Espera que la función pasada también ajuste su valor de retorno en el mismo tipo de clase genérica. Para ayudar con eso, proporciona createun constructor que puede crear una instancia de esa clase genérica a partir de un valor. El resultado de devolución de flatMap también es una clase genérica del mismo tipo, que a menudo empaqueta los mismos valores contenidos en los resultados de devolución de una o más aplicaciones de flatMap a los valores previamente contenidos. Esto le permite encadenar flatMap tanto como desee:

intList.flatMap(x => List<int>.makeFromTwo(x, x*10))
       .flatMap(x => x % 3 == 0 
                   ? List<string>.create("x = " + x.toString()) 
                   : List<string>.empty())

Sucede que este tipo de clase genérica es útil como modelo base para una gran cantidad de cosas. Esta (junto con las jergas de la teoría de categorías) es la razón por la cual las mónadas parecen tan difíciles de entender o explicar. Son algo muy abstracto y solo se vuelven obviamente útiles una vez que se especializan.

Por ejemplo, puede modelar excepciones utilizando contenedores monádicos. Cada contenedor contendrá el resultado de la operación o el error que ha ocurrido. La siguiente función (delegado) en la cadena de devoluciones de llamada de flatMap solo se llamará si la anterior incluyó un valor en el contenedor. De lo contrario, si se empaquetó un error, el error continuará propagándose a través de los contenedores encadenados hasta que se encuentre un contenedor que tenga una función de controlador de errores adjuntada mediante un método llamado .orElse()(dicho método sería una extensión permitida)

Notas : Los lenguajes funcionales le permiten escribir funciones que pueden operar en cualquier clase de clase genérica monádica. Para que esto funcione, uno tendría que escribir una interfaz genérica para mónadas. No sé si es posible escribir una interfaz de este tipo en C #, pero que yo sepa no lo es:

interface IMonad<T> { 
    static IMonad<T> create(T t); // not allowed
    public IMonad<U> flatMap<U>(Func<T, IMonad<U>> f); // not specific enough,
    // because the function must return the same kind of monad, not just any monad
}

7

Si una mónada tiene una interpretación "natural" en OO depende de la mónada. En un lenguaje como Java, puede traducir la mónada quizás al lenguaje de verificación de punteros nulos, de modo que los cálculos que fallan (es decir, no producen nada en Haskell) emitan punteros nulos como resultado. Puede traducir la mónada de estado al lenguaje generado creando una variable mutable y métodos para cambiar su estado.

Una mónada es un monoide en la categoría de endofunctores.

La información que reúne la oración es muy profunda. Y trabajas en una mónada con cualquier lenguaje imperativo. Una mónada es un lenguaje específico de dominio "secuenciado". Satisface ciertas propiedades interesantes, que en conjunto hacen de una mónada un modelo matemático de "programación imperativa". Haskell facilita la definición de lenguajes imperativos pequeños (o grandes), que se pueden combinar de varias maneras.

Como programador de OO, utiliza la jerarquía de clases de su lenguaje para organizar los tipos de funciones o procedimientos que pueden llamarse en un contexto, lo que llama un objeto. Una mónada es también una abstracción de esta idea, en la medida en que diferentes mónadas se pueden combinar de manera arbitraria, "importando" efectivamente todos los métodos de la submónada al alcance.

Arquitectónicamente, uno usa firmas de tipo para expresar explícitamente qué contextos pueden usarse para calcular un valor.

Uno puede usar transformadores de mónada para este propósito, y hay una colección de alta calidad de todas las mónadas "estándar":

  • Listas (cálculos no deterministas, tratando una lista como un dominio)
  • Tal vez (cálculos que pueden fallar, pero para los cuales los informes no son importantes)
  • Error (cálculos que pueden fallar y requieren manejo de excepciones
  • Lector (cálculos que pueden representarse mediante composiciones de funciones simples de Haskell)
  • Escritor (cálculos con "representación" / "registro" secuencial (en cadenas, html, etc.)
  • Cont (continuaciones)
  • IO (cálculos que dependen del sistema informático subyacente)
  • Estado (cálculos cuyo contexto contiene un valor modificable)

con transformadores de mónada correspondientes y clases de tipos. Las clases de tipos permiten un enfoque complementario para combinar mónadas al unificar sus interfaces, de modo que las mónadas concretas puedan implementar una interfaz estándar para el "tipo" de mónada. Por ejemplo, el módulo Control.Monad.State contiene una clase MonadState sm y (State s) es una instancia del formulario

instance MonadState s (State s) where
    put = ...
    get = ...

La larga historia es que una mónada es un functor que atribuye "contexto" a un valor, que tiene una forma de inyectar un valor en la mónada, y que tiene una forma de evaluar valores con respecto al contexto adjunto, al menos de forma restringida

Entonces:

return :: a -> m a

es una función que inyecta un valor de tipo a en una "acción" de mónada de tipo m a.

(>>=) :: m a -> (a -> m b) -> m b

es una función que realiza una acción de mónada, evalúa su resultado y aplica una función al resultado. Lo bueno de (>> =) es que el resultado está en la misma mónada. En otras palabras, en m >> = f, (>> =) extrae el resultado de m y lo une a f, de modo que el resultado está en la mónada. (Alternativamente, podemos decir que (>> =) extrae f en m y lo aplica al resultado.) Como consecuencia, si tenemos f :: a -> mb y g :: b -> mc, podemos acciones de "secuencia":

m >>= f >>= g

O, usando "hacer notación"

do x <- m
   y <- f x
   g y

El tipo para (>>) puede ser esclarecedor. Está

(>>) :: m a -> m b -> m b

Corresponde al operador (;) en lenguajes de procedimiento como C. Permite hacer anotaciones como:

m = do x <- someQuery
       someAction x
       theNextAction
       andSoOn

En lógica matemática y filosófica, tenemos marcos y modelos, que son "naturalmente" modelados con el monadismo. Una interpretación es una función que examina el dominio del modelo y calcula el valor de verdad (o generalizaciones) de una proposición (o fórmula, bajo generalizaciones). En una lógica modal de necesidad, podríamos decir que una proposición es necesaria si es cierta en "todos los mundos posibles", si es cierta con respecto a cada dominio admisible. Esto significa que un modelo en un lenguaje para una proposición puede reificarse como un modelo cuyo dominio consiste en la colección de modelos distintos (uno correspondiente a cada mundo posible). Cada mónada tiene un método llamado "unirse" que aplana las capas, lo que implica que cada acción de mónada cuyo resultado es una acción de mónada puede integrarse en la mónada.

join :: m (m a) -> m a

Más importante aún, significa que la mónada está cerrada bajo la operación de "apilamiento de capas". Así es como funcionan los transformadores de mónada: combinan mónadas al proporcionar métodos de "unión" para tipos como

newtype MaybeT m a = MaybeT { runMaybeT :: m (Maybe a) }

para que podamos transformar una acción en (Quizás T m) en una acción en m, colapsando efectivamente las capas. En este caso, runMaybeT :: MaybeT ma -> m (Quizás a) es nuestro método de unión. (MaybeT m) es una mónada, y MaybeT :: m (Quizás a) -> MaybeT ma es efectivamente un constructor de un nuevo tipo de acción de mónada en m.

Una mónada libre para un functor es la mónada generada al apilar f, con la implicación de que cada secuencia de constructores para f es un elemento de la mónada libre (o, más exactamente, algo con la misma forma que el árbol de secuencias de constructores para F). Las mónadas libres son una técnica útil para construir mónadas flexibles con una cantidad mínima de placa de caldera. En un programa de Haskell, podría usar mónadas gratuitas para definir mónadas simples para la "programación de sistemas de alto nivel" para ayudar a mantener la seguridad de los tipos (solo estoy usando los tipos y sus declaraciones. Las implementaciones son sencillas con el uso de combinadores):

data RandomF r a = GetRandom (r -> a) deriving Functor
type Random r a = Free (RandomF r) a


type RandomT m a = Random (m a) (m a) -- model randomness in a monad by computing random monad elements.
getRandom     :: Random r r
runRandomIO   :: Random r a -> IO a (use some kind of IO-based backend to run)
runRandomIO'  :: Random r a -> IO a (use some other kind of IO-based backend)
runRandomList :: Random r a -> [a]  (some kind of list-based backend (for pseudo-randoms))

El monadismo es la arquitectura subyacente de lo que se podría llamar el patrón de "intérprete" o "comando", abstraído en su forma más clara, ya que cada cálculo monádico debe "ejecutarse", al menos trivialmente. (El sistema de tiempo de ejecución ejecuta la mónada IO para nosotros y es el punto de entrada para cualquier programa Haskell. IO "impulsa" el resto de los cálculos, ejecutando acciones IO en orden).

El tipo para unirse también es donde obtenemos la afirmación de que una mónada es un monoide en la categoría de endofunctores. La unión suele ser más importante para fines teóricos, en virtud de su tipo. Pero comprender el tipo significa comprender las mónadas. Los tipos de unión y transformador de mónada son efectivamente composiciones de endofunctores, en el sentido de composición de funciones. Para ponerlo en un pseudo-lenguaje tipo Haskell,

Foo :: m (ma) <-> (m. M) a


3

Una mónada es un conjunto de funciones.

(Pst: una matriz de funciones es solo un cálculo).

En realidad, en lugar de una matriz verdadera (una función en una matriz de celdas) tiene esas funciones encadenadas por otra función >> =. >> = permite adaptar los resultados de la función i a la función de alimentación i + 1, realizar cálculos entre ellos o, incluso, no llamar a la función i + 1.

Los tipos utilizados aquí son "tipos con contexto". Esto es, un valor con una "etiqueta". Las funciones que se encadenan deben tomar un "valor desnudo" y devolver un resultado etiquetado. Una de las tareas de >> = es extraer un valor desnudo de su contexto. También existe la función "return", que toma un valor desnudo y lo coloca con una etiqueta.

Un ejemplo con Quizás . Usémoslo para almacenar un número entero simple en el que hacer cálculos.

-- a * b
multiply :: Int -> Int -> Maybe Int
multiply a b = return  (a*b)

-- divideBy 5 100 = 100 / 5
divideBy :: Int -> Int -> Maybe Int
divideBy 0 _ = Nothing -- dividing by 0 gives NOTHING
divideBy denom num = return (quot num denom) -- quotient of num / denom

-- tagged value
val1 = Just 160 

-- array of functions feeded with val1
array1 = val1 >>= divideBy 2  >>= multiply 3 >>= divideBy  4 >>= multiply 3

-- array of funcionts created with the do notation
-- equals array1 but for the feeded val1
array2 :: Int -> Maybe Int
array2 n = do
       v <- divideBy 2  n
       v <- multiply 3 v
       v <- divideBy 4 v
       v <- multiply 3 v
       return v

-- array of functions, 
-- the first >>= performs 160 / 0, returning Nothing
-- the second >>= has to perform Nothing >>= multiply 3 ....
-- and simply returns Nothing without calling multiply 3 ....
array3 = val1 >>= divideBy 0  >>= multiply 3 >>= divideBy  4 >>= multiply 3

main = do
     print array1
     print (array2 160)
     print array3

Solo para mostrar que las mónadas son un conjunto de funciones con operaciones auxiliares, considere el equivalente al ejemplo anterior, simplemente usando un conjunto real de funciones

type MyMonad = [Int -> Maybe Int] -- my monad as a real array of functions

myArray1 = [divideBy 2, multiply 3, divideBy 4, multiply 3]

-- function for the machinery of executing each function i with the result provided by function i-1
runMyMonad :: Maybe Int -> MyMonad -> Maybe Int
runMyMonad val [] = val
runMyMonad Nothing _ = Nothing
runMyMonad (Just val) (f:fs) = runMyMonad (f val) fs

Y se usaría así:

print (runMyMonad (Just 160) myArray1)

1
Súper ordenado! Entonces bind es solo una forma de evaluar una serie de funciones con contexto, en secuencia, en una entrada con contexto :)
Musa Al-hassy

>>=es un operador
usuario2418306

1
Creo que la analogía del "conjunto de funciones" no aclara mucho. Si \x -> x >>= k >>= l >>= mes un conjunto de funciones, también lo es h . g . f, lo que no involucra a las mónadas en absoluto.
duplode

podríamos decir que los functores , ya sean monádicos, aplicativos o simples, tratan sobre "aplicación embellecida" . 'aplicativo' agrega encadenamiento, y 'mónada' agrega dependencia (es decir, crea el siguiente paso de cálculo dependiendo de los resultados de un paso de cálculo anterior).
Will Ness

3

En términos OO, una mónada es un contenedor fluido.

El requisito mínimo es una definición de class <A> Somethingque admite un constructor Something(A a)y al menos un métodoSomething<B> flatMap(Function<A, Something<B>>)

Podría decirse que también cuenta si su clase mónada tiene algún método con firma Something<B> work()que conserve las reglas de la clase: el compilador hornea en flatMap en el momento de la compilación.

¿Por qué es útil una mónada? Porque es un contenedor que permite operaciones en cadena que preservan la semántica. Por ejemplo, Optional<?>conserva la semántica de isPresent para Optional<String>, Optional<Integer>, Optional<MyClass>, etc.

Como un ejemplo aproximado,

Something<Integer> i = new Something("a")
  .flatMap(doOneThing)
  .flatMap(doAnother)
  .flatMap(toInt)

Tenga en cuenta que comenzamos con una cadena y terminamos con un número entero. Muy genial.

En OO, puede tomar un pequeño movimiento de mano, pero cualquier método en Something que devuelva otra subclase de Something cumple con el criterio de una función contenedor que devuelve un contenedor del tipo original.

Así es como se preserva la semántica, es decir, el significado y las operaciones del contenedor no cambian, simplemente envuelven y mejoran el objeto dentro del contenedor.


2

Las mónadas en uso típico son el equivalente funcional de los mecanismos de manejo de excepciones de la programación de procedimientos.

En los lenguajes de procedimiento modernos, coloca un controlador de excepciones alrededor de una secuencia de declaraciones, cualquiera de las cuales puede generar una excepción. Si alguna de las declaraciones arroja una excepción, la ejecución normal de la secuencia de declaraciones se detiene y transfiere a un controlador de excepciones.

Sin embargo, los lenguajes de programación funcional evitan filosóficamente las funciones de manejo de excepciones debido a su naturaleza "goto". La perspectiva de la programación funcional es que las funciones no deberían tener "efectos secundarios" como excepciones que interrumpen el flujo del programa.

En realidad, los efectos secundarios no se pueden descartar en el mundo real debido principalmente a E / S. Las mónadas en la programación funcional se utilizan para manejar esto tomando un conjunto de llamadas de función encadenadas (cualquiera de las cuales puede producir un resultado inesperado) y convirtiendo cualquier resultado inesperado en datos encapsulados que aún pueden fluir de manera segura a través de las llamadas de función restantes.

El flujo de control se conserva, pero el evento inesperado se encapsula y maneja de forma segura.


2

Una explicación simple de Monads con el estudio de caso de Marvel está aquí .

Las mónadas son abstracciones utilizadas para secuenciar funciones dependientes que son efectivas. Efectivo aquí significa que devuelven un tipo en forma F [A], por ejemplo, Opción [A] donde Opción es F, llamada constructor de tipo. Veamos esto en 2 simples pasos

  1. Debajo la composición de la función es transitiva. Entonces, pasar de A a CI puede componer A => B y B => C.
 A => C   =   A => B  andThen  B => C

ingrese la descripción de la imagen aquí

  1. Sin embargo, si la función devuelve un tipo de efecto como la Opción [A], es decir, A => F [B], la composición no funciona para ir a B, necesitamos A => B, pero tenemos A => F [B].
    ingrese la descripción de la imagen aquí

    Necesitamos un operador especial, "bind" que sepa cómo fusionar estas funciones que devuelven F [A].

 A => F[C]   =   A => F[B]  bind  B => F[C]

La función "vincular" se define para la F específica .

También hay "retorno" , de tipo A => F [A] para cualquier A , definido para esa F específica también. Para ser una mónada, F debe tener definidas estas dos funciones.

Por lo tanto, podemos construir una función efectiva A => F [B] a partir de cualquier función pura A => B ,

 A => F[B]   =   A => B  andThen  return

pero una F dada también puede definir sus propias funciones especiales "integradas" opacas de tales tipos que un usuario no puede definirse a sí mismo (en un lenguaje puro ), como

  • "random" ( Rango => Aleatorio [Int] )
  • "print" ( Cadena => IO [()] )
  • "intenta ... atrapa", etc.

2

Estoy compartiendo mi comprensión de las mónadas, que puede no ser teóricamente perfecta. Las mónadas tratan sobre la propagación del contexto . Mónada es, usted define algún contexto para algunos datos (o tipos de datos), y luego define cómo se llevará ese contexto con los datos a lo largo de su canal de procesamiento. Y definir la propagación del contexto se trata principalmente de definir cómo fusionar múltiples contextos (del mismo tipo). Usar Monads también significa garantizar que estos contextos no se eliminen accidentalmente de los datos. Por otro lado, otros datos sin contexto pueden llevarse a un contexto nuevo o existente. Entonces, este concepto simple puede usarse para asegurar la corrección del tiempo de compilación de un programa.



1

Vea mi respuesta a "¿Qué es una mónada?"

Comienza con un ejemplo motivador, funciona a través del ejemplo, deriva un ejemplo de una mónada y define formalmente "mónada".

No asume ningún conocimiento de programación funcional y utiliza pseudocódigo con function(argument) := expressionsintaxis con las expresiones más simples posibles.

Este programa C ++ es una implementación de la mónada pseudocódigo. (Para referencia: Mes el constructor de tipo, feedes la operación "vincular" y wrapes la operación "retorno").

#include <iostream>
#include <string>

template <class A> class M
{
public:
    A val;
    std::string messages;
};

template <class A, class B>
M<B> feed(M<B> (*f)(A), M<A> x)
{
    M<B> m = f(x.val);
    m.messages = x.messages + m.messages;
    return m;
}

template <class A>
M<A> wrap(A x)
{
    M<A> m;
    m.val = x;
    m.messages = "";
    return m;
}

class T {};
class U {};
class V {};

M<U> g(V x)
{
    M<U> m;
    m.messages = "called g.\n";
    return m;
}

M<T> f(U x)
{
    M<T> m;
    m.messages = "called f.\n";
    return m;
}

int main()
{
    V x;
    M<T> m = feed(f, feed(g, wrap(x)));
    std::cout << m.messages;
}

0

Desde un punto de vista práctico (que resume lo que se ha dicho en muchas respuestas anteriores y artículos relacionados), me parece que uno de los "propósitos" (o utilidad) fundamentales de la mónada es aprovechar las dependencias implícitas en las invocaciones de métodos recursivos también conocida como composición de funciones (es decir, cuando f1 llama a f2 llama a f3, f3 debe evaluarse antes de f2 antes de f1) para representar la composición secuencial de forma natural, especialmente en el contexto de un modelo de evaluación diferida (es decir, la composición secuencial como una secuencia simple , por ejemplo, "f3 (); f2 (); f1 ();" en C: el truco es especialmente obvio si piensa en un caso en el que f3, f2 y f1 realmente no devuelven nada [encadenados como f1 (f2 (f3)) es artificial, puramente destinado a crear secuencia]).

Esto es especialmente relevante cuando los efectos secundarios están involucrados, es decir, cuando se altera algún estado (si f1, f2, f3 no tuvieron efectos secundarios, no importaría en qué orden se evalúan; lo cual es una gran propiedad de puro lenguajes funcionales, para poder paralelizar esos cálculos, por ejemplo). Cuantas más funciones puras, mejor.

Creo que desde ese estrecho punto de vista, las mónadas podrían verse como azúcar sintáctica para los idiomas que favorecen la evaluación diferida (que evalúan las cosas solo cuando es absolutamente necesario, siguiendo un orden que no se basa en la presentación del código), y que no tienen Otros medios para representar la composición secuencial. El resultado neto es que las secciones de código que son "impuras" (es decir, que tienen efectos secundarios) se pueden presentar de forma natural, de manera imperativa, pero se separan limpiamente de las funciones puras (sin efectos secundarios), que pueden ser evaluado perezosamente.

Sin embargo, este es solo un aspecto, como se advirtió aquí .


0

La explicación más simple que se me ocurre es que las mónadas son una forma de componer funciones con resultados embellecidos (también conocida como composición de Kleisli). Una función "embellecido" tiene la firma a -> (b, smth), donde ay bson tipos (pensar Int, Bool) que pueden ser diferentes entre sí, pero no necesariamente - y smthes el "contexto" o el "embelishment".

Este tipo de funciones también se puede escribir a -> m bdonde mes equivalente a la "embellecimiento" smth. Entonces, estas son funciones que devuelven valores en contexto (piense en funciones que registran sus acciones, dónde smthestá el mensaje de registro; o funciones que realizan entrada / salida y sus resultados dependen del resultado de la acción IO).

Una mónada es una interfaz ("typeclass") que hace que el implementador le diga cómo componer dichas funciones. El implementador necesita definir una función de composición (a -> m b) -> (b -> m c) -> (a -> m c)para cualquier tipo mque quiera implementar la interfaz (esta es la composición de Kleisli).

Entonces, si decimos que tenemos un tipo de tupla que (Int, String)representa los resultados de los cálculos en Ints que también registran sus acciones, (_, String)siendo el "embellecimiento" - el registro de la acción - y dos funciones increment :: Int -> (Int, String)y twoTimes :: Int -> (Int, String)queremos obtener una función incrementThenDouble :: Int -> (Int, String)que es la composición de las dos funciones que también tiene en cuenta los registros.

En el ejemplo dado, una implementación de mónada de las dos funciones se aplica al valor entero 2 incrementThenDouble 2(que es igual a twoTimes (increment 2)) que arrojaría (6, " Adding 1. Doubling 3.")resultados intermedios increment 2iguales (3, " Adding 1.")e twoTimes 3iguales a(6, " Doubling 3.")

De esta función de composición de Kleisli se pueden derivar las funciones monádicas habituales.

Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.