Pregunta ¿Mónada en inglés llano? (Para el programador OOP sin antecedentes FP)


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

¿Qué problema soluciona 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?


563
2018-04-24 13:42


origen


Respuestas:


ACTUALIZACIÓN: Esta pregunta fue el tema de una serie de blogs inmensamente larga, que puedes leer en Mónadas - gracias por la gran pregunta!

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

Una mónada es una "amplificador" de tipos ese obedece ciertas reglas y que tiene ciertas operaciones provistas.

Primero, ¿qué es un "amplificador de tipos"? Con eso me refiero a algún sistema que te 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. Te permite tomar un tipo, decir int, y agregue una nueva capacidad a ese tipo, es decir, que ahora puede ser nulo cuando antes no podía.

Como segundo ejemplo, considere IEnumerable<T>. Es un amplificador de tipos. Te permite tomar un tipo, por ejemplo, string, y agregue una nueva capacidad a ese tipo, es decir, que ahora puede hacer una secuencia de cadenas de cualquier cantidad de cadenas individuales.

¿Cuáles son las "ciertas reglas"? En resumen, existe una forma sensata para que las funciones del tipo subyacente funcionen en el tipo amplificado de modo que sigan las reglas normales de la 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 trabajen juntos "de la misma manera" que antes.

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

¿Cuáles son las "operaciones"?

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

  2. Hay una operación de "enlace" que toma un valor monádico y una función que puede transformar el valor, y devuelve un nuevo valor monádico. Bind 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 a las reglas de composición funcional mencionadas anteriormente.

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

De nuevo, toma Nullable<T> como ejemplo. Puedes convertir un int en un Nullable<int> con el constructor El compilador de C # se encarga de la mayoría de los "levantamientos" que aceptan nulos para usted, pero si no lo hizo, la transformación de elevación 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 convirtiendo un Nullable<int> de vuelta a una int se hace con el Value propiedad.

La transformación de funciones es la clave. Observe cómo la semántica real de la operación anulable - que una operación en un null propaga el null - es capturado en la transformación. Podemos generalizar esto.

Supongamos que tiene una función de int a int, como nuestro original M. Puedes convertirlo fácilmente en una función que toma una int y devuelve un Nullable<int> porque solo puedes ejecutar el resultado a través del constructor que admite nulos. Ahora supongamos 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 toma una int y devuelve un int, o toma una int y devuelve un Nullable<int> ahora puede tener la semántica con nulos aplicable.

Además: supongamos 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, Z es la composición de X y Y. Pero no puedes hacer eso porque X toma una inty Y devuelve un Nullable<int>. Pero como tiene la operación de "enlace", 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 funcione la composición de funciones en tipos amplificados. Las "reglas" que he indicado anteriormente son que la mónada preserva las reglas de la composición de funciones normales; que la composición con funciones de identidad da como resultado la función original, esa composición es asociativa, y así sucesivamente.

En C #, "Bind" se llama "SelectMany". Eche un vistazo a cómo funciona en la secuencia de mónada. Necesitamos tener dos cosas: convertir un valor en una secuencia y enlazar operaciones en secuencias. Como beneficio adicional, también tenemos "convertir una secuencia 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 la mónada anulable era "combinar dos funciones que producen nulables juntas, compruebe si el interno da como resultado nulo; si lo hace, produce nulo, si no lo hace, luego llame al externo con el resultado". Esa es la semántica deseada de Nullable.

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 el Bind/SelectMany métodos; este es el método que te dice qué es realmente la mónada medio.

Podemos hacer aún mejor. Supongamos que tiene una secuencia de entradas y un método que toma datos y resultados en 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 y cuando las entradas de uno coincidan con las salidas del otro:

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 "amplificar este conjunto de enteros individuales en una secuencia de enteros. Transforma este entero particular en un montón de cadenas, amplificadas a una secuencia de cadenas. Ahora junta ambas operaciones: amplifica este conjunto de enteros en la concatenación de todas las secuencias de cuerdas ". Las mónadas te permiten componer tus amplificaciones

¿Qué problema soluciona 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 voy a intentarlo.

Las mónadas se usan generalmente para resolver problemas como:

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

C # usa mónadas en su diseño. Como ya se mencionó, el patrón que admite nulos es muy similar a la "quizás mónada". LINQ está completamente construido a partir de mónadas; el SelectMany método es lo que hace el trabajo semántico de la composición de las operaciones. (A Erik Meijer le gusta señalar que cada función LINQ podría ser implementada por 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 directamente el patrón de mónadas; necesita un sistema de tipo que admita tipos que son tipos superiores a los tipos 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, (quizás) 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 SelectMany método; es la clave para entender cómo funciona la secuencia de mónada en C #. Es un método muy simple, ¡pero muy poderoso!


Sugerido, lectura adicional:

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


608
2018-01-25 10:42



¿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 lo que se debe ejecutar primero? ¿Cómo podemos formar una secuencia ordenada de funciones (es decir, un programa) usando solo funciones?

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

  3. Más problemas: algunas funciones puede fallar (es decir. g(2,0), divide por 0). Tenemos sin "excepciones" en FP. ¿Como lo resolvemos?

    Solución: Vamos a Permitir que las funciones devuelvan dos tipos de cosas: En vez de tener g : Real,Real -> Real (función de dos reales en un real), vamos a permitir 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 tal vez un real o simplemente no sea nada. Por lo tanto, podemos tener g : Real,Real -> Maybe Real. Bien pero ...

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

    Solución: vamos a tiene 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 g a f) Queremos >>= Llegar gsalida, inspecciónelo y, en caso de que sea Nothing simplemente no llame f y volver Nothing; o, por el contrario, extraiga la caja Real y alimentación f con eso. (Este algoritmo es solo la implementación de >>= Para el Maybe tipo).

  6. Surgen muchos otros problemas que se pueden resolver usando este mismo patrón: 1. Use una "caja" para codificar / almacenar diferentes significados / valores, y tenga funciones como g que devuelven esos "valores encuadrados". 2. Tener compositores / vinculadores g >>= f para ayudar a conectar gSalida de fde entrada, por lo que no tenemos que cambiar f en absoluto.

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

    • tener un estado global que todas las funciones en la secuencia de funciones ("el programa") puedan compartir: solución StateMonad.

    • No nos gustan las "funciones impuras": funciones que producen diferente salida para mismo entrada. Por lo tanto, marquemos esas funciones, haciendo que devuelvan un valor etiquetado / encuadrado: IO monada.

Felicidad total !!!!


213
2017-07-17 00:14



En términos que un programador OOP haría   entender (sin funcional   fondo de programación), ¿qué es un   ¿monada?

¿Qué problema soluciona y qué   Cuáles son los lugares más comunes donde 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 una mixin), parametrizada por un tipo, con dos métodos, return y bind que describe:

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

El problema que resuelve es el mismo tipo de problema que esperaría de cualquier interfaz, es decir, "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. ¿Cómo puedo describir esa similitud entre ellos, incluso si las clases en sí mismas no son realmente subtipos de nada? más cerca que la clase 'el Objeto' en sí? "

Más específicamente, el Monad "interfaz" es similar a IEnumerator o IIterator en eso toma un tipo que toma un tipo. El principal "punto" de Monad sin embargo, es capaz de conectar operaciones basadas en el tipo de interior, incluso hasta el punto de tener un nuevo "tipo interno", mientras mantiene, o incluso mejora, la estructura de información de la clase principal.


58
2018-05-19 17:58



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

En el patrón de comando, envuelve una declaración o expresión ordinaria en un mando objeto. El objeto de comando expone una ejecutar método que ejecuta la declaración envuelta. Por lo tanto, la declaración se convierte en objetos de primera clase que pueden pasarse y ejecutarse a voluntad. Los comandos pueden ser compuesto 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 los diferentes invocadores pueden aplicar una lógica diferente a cómo se deben ejecutar los comandos.

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

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

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


48
2017-07-17 22:39



Tienes una presentación reciente "Monadologie - ayuda profesional sobre la ansiedad tipo"por Christopher League (12 de julio de 2010), que es bastante interesante sobre temas de continuación y mónada.
El video que va con esta presentación (slideshare) es en realidad 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 a la vez OOP y funcional.
Puedes leer más sobre Monad en Scala en la publicación del blog "Mónadas: otra forma de abstraer cálculos en Scala", de Debasish Ghosh (27 de marzo de 2008).

Un tipo constructor 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
    def 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 Algunos (x) => f (x)
      }
  • List es Monad
    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)
      }

La mónada es un gran problema en Scala debido a la conveniente sintaxis construida para aprovechar las estructuras de la mónada:

for comprensió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 la flatMap, que une el cálculo a través del encadenamiento.
Cada invocación de flatMap devuelve el mismo tipo de estructura de datos (pero de diferente valor), que sirve como entrada para el siguiente comando en la cadena.

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

Esto es lo que "une" el hilo de cálculo: cada elemento de la secuencia en la comprensión tiene que respetar este mismo tipo de restricción.


Si toma dos operaciones (que pueden fallar) y pasa el resultado al tercero, como:

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

pero sin tomar ventaja de Monad, obtienes un código OOP intrincado 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, puedes trabajar con los tipos reales (Venue, User) como todas las operaciones funcionan, y mantener oculta la opción de verificación de opciones, todo debido a los mapas 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]; alguna None sería devuelto directamente a confirm.


Asi que:

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 una 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 el enhebrado de acciones por una mónada es realizada por el compilador del lenguaje que realiza la transformación a través de la magia de los cierres.


Por cierto, Monad no es solo el modelo de computación utilizado en FP: vea esto entrada en el blog.

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

  • el modelo de cómputo Arrow
  • el modelo de cómputo Monad
  • el modelo aplicativo de cálculos

35
2018-06-06 04:14



He escrito un breve artículo que compara el código python OOP estándar con el código python monádico que demuestra el proceso computacional subyacente con diagramas. No asume ningún conocimiento previo de FP. Esperamos que te sea útil - http://nikgrozev.com/2013/12/10/monads-in-15-minutes/


28
2018-05-20 11:17



Para respetar a los lectores rápidos, comienzo primero con una definición precisa, continúe con una explicación más rápida de "inglés sencillo", y luego vaya a ejemplos.

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

UN monada (en informática) es formalmente un mapa que:

  • envía todo tipo X de algún lenguaje de programación dado a un nuevo tipo T(X) (llamado el "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) a una función g∘f:X->T(Z);

  • de una manera que sea asociativa en el sentido evidente y unital con respecto a una función de unidad dada llamada pure_X:X->T(X), para ser considerado como un valor para el cálculo puro que simplemente devuelve ese valor.

Entonces en palabras simples, un monada es un regla para pasar de cualquier tipo X a otro tipo T(X)y un 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ón h:X->T(Z). Que, sin embargo, no es la composición en estricto sentido matemático. Estamos básicamente "doblando" la composición de la función o redefiniendo cómo se componen las funciones.

Además, necesitamos la regla de composición de la mónada para satisfacer los axiomas matemáticos "obvios":

  • Asociatividad: Componer f con g y luego con h (desde afuera) debe ser lo mismo que componer g con h y luego con f (desde adentro).
  • Propiedad unital: Componer f con el identidad función en cualquier lado debe rendir f.

Una vez más, en palabras simples, no podemos enloquecer redefinir la composición de nuestra función como queramos:

  • Primero necesitamos la asociatividad para poder componer varias funciones seguidas, p. f(g(h(k(x))), y no preocuparse por especificar los pares de funciones de composición de órdenes. Como la regla de mónada solo prescribe cómo componer un par de funcionesSin ese axioma, necesitaríamos saber qué par está compuesto primero y así sucesivamente. (Tenga en cuenta que es diferente de la propiedad de conmutatividad que f compuesto con g eran lo mismo que g compuesto con f, que no es obligatorio).
  • Y en segundo lugar, necesitamos la propiedad unital, que es simplemente decir que las identidades se componen trivialmente de la manera en que las esperamos. Por lo tanto, podemos refactorizar de forma segura las funciones siempre que se puedan extraer esas identidades.

Entonces, de nuevo en resumen: una mónada es la regla de las funciones de extensión y composición del tipo que satisface los dos axiomas: asociatividad y propiedad unital.

En términos prácticos, quiere que el lenguaje, el compilador o el marco de trabajo implementen la mónada que se encargaría de usted para la composición de funciones. De modo que puede concentrarse en escribir la lógica de su función en lugar de preocuparse por cómo se implementa su ejecución.

Eso es esencialmente, en pocas palabras.


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


Monad es no es un funtor! Un funtor F es una regla para ir de tipo X digitar F(X) y funciones (morfismo) entre tipos X y Y a funciones entre F(X) y F(Y) (enviando objetos a objetos y sus morfismos a morfismos en la teoría de categorías). En cambio, una mónada envía un par de funciones f y g a uno nuevo h.


Para que sea menos seco, permítanme tratar de ilustrarlo con el ejemplo que estoy anotando con secciones pequeñas, por lo que puede pasar directamente al punto.

Ejecución 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, entonces una excepción e es aventado. Entonces, ¿cómo se puede definir el valor de la composición? g(f(0))? ¡Lanza una excepción otra vez, por supuesto! Quizás lo mismo e. Tal vez una nueva excepción actualizada e1.

¿Qué ocurre exactamente aquí? Primero, necesitamos nuevos valores de excepción (diferentes o iguales). Puedes llamarlos nothing o null o lo que sea, pero la esencia sigue siendo la misma: deberían ser valores nuevos, p. no debería ser un number en nuestro ejemplo aquí. Prefiero no llamarlos null para evitar confusiones con la forma null se puede implementar en cualquier idioma específico. Igualmente prefiero evitar nothing porque a menudo se asocia con null, que, en principio, es lo que null debería hacer, sin embargo, ese principio a menudo se dobla por cualquier razón práctica.

¿Cuál es la excepción exactamente?

Esta es una cuestión trivial para cualquier programador experimentado, pero me gustaría soltar algunas palabras solo para apagar 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 desechar cualquier detalle y devolver un solo valor global (como NaN o null) o generar una larga lista de logs o lo que sucedió exactamente, enviarla a una base de datos y replicarla 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 hay sin efectos secundarios. En el segundo hay Lo que nos lleva a la pregunta (mil dólares):

¿Se permiten excepciones en funciones puras?

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

Respuesta más larga. Para ser puro, la salida de su función debe ser determinada de manera única por su entrada. Entonces modificamos nuestra función f enviando 0 al nuevo valor abstracto e que llamamos excepción. Nos aseguramos de que ese valor e no contiene información externa que no está determinada únicamente por nuestra información, que es x. Así que aquí hay un ejemplo de excepción sin efecto secundario:

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

Y aquí hay uno con efecto secundario:

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 posiblemente cambiar en el futuro. Pero si se garantiza que nunca cambiará, ese valor se vuelve únicamente predecible, y por lo tanto no hay efectos secundarios.

Para hacerlo aún más tonto. Una función que regresa 42 alguna vez es claramente puro. Pero si alguien loco decide hacer 42 una variable que ese valor puede cambiar, la misma función deja de ser pura bajo las nuevas condiciones.

Tenga en cuenta que estoy usando la notación literal del objeto para simplificar la demostración de la esencia. Lamentablemente, las cosas están mal en idiomas como JavaScript, donde error no es un tipo que se comporta de la manera que queremos aquí con respecto a la composición de la función, mientras que los tipos reales como null o NaN no se comporte de esta manera, sino que realice las conversiones de tipo artificial y no siempre intuitivo.

Tipo de extensión

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

¿Qué es la composición funcional?

Es la operación matemática que toma funciones f: X -> Y y g: Y -> Z y construyendo su composición como función h: X -> Z satisfactorio 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 trabajo adicional. La solución estrictamente matemática para nuestro ejemplo anterior de f y g es eliminar 0 del conjunto de definiciones de f. Con ese nuevo conjunto de definiciones (nuevo tipo más restrictivo de x), f se vuelve composable con g.

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

O como otro enfoque, se crean valores artificiales como NaN, undefined, null, Infinity etc. Entonces usted evalúa 1/0 a Infinity y 1/-0 a -Infinity. Y luego fuerce el nuevo valor en su expresión en lugar de lanzar una excepción. Llevando a resultados que puede o no puede predecir:

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 lanzar 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 para componer funciones que satisfagan los axiomas definidos al principio de esta respuesta.

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

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

¿Se puede usar throwing exception para construir una mónada?

De hecho, una mónada más útil sería la regla que prescribe eso si f lanza excepción para algunos x, también lo hace su composición con cualquier g. Además haz la excepción E globalmente único con un solo valor posible jamás (objeto terminal en la teoría de categorías). Ahora los dos axiomas son verificables instantáneamente y obtenemos una mónada muy útil. Y el resultado es lo que se conoce como el tal vez mónada.


21
2018-04-24 19:37



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

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

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


18
2018-04-24 13:45



De wikipedia:

En la programación funcional, una mónada es   un tipo de tipo de datos abstracto utilizado para   representar cálculos (en lugar de   datos en el modelo de dominio). Mónadas   permitir al programador encadenar acciones   juntos para construir una tubería, en la cual   cada acción está decorada con   reglas de procesamiento adicionales proporcionadas   por la mónada Programas escritos en   estilo funcional puede hacer uso de   mónadas para estructurar procedimientos que   incluir operaciones secuenciadas,1[2]   o para definir flujos de control arbitrarios   (como manejar concurrencia,   continuación, o excepciones).

Formalmente, una mónada es construida por   definir dos operaciones (vincular y   return) y un constructor de tipo M que   debe cumplir varias propiedades para   permitir la correcta composición de   funciones monádicas (es decir, funciones que   usar valores de la mónada como su   argumentos). La operación de devolución toma   un valor de un tipo simple y lo pone   en un contenedor monádico de tipo M.   La operación de vinculación realiza el   proceso inverso, extrayendo el   valor original del contenedor y   pasándolo al próximo asociado   función en la tubería.

Un programador compondrá monádico   funciones para definir un procesamiento de datos   tubería. La mónada actúa como un   marco, ya que es un comportamiento reutilizable   eso decide el orden en que el   funciones monádicas específicas en el   se llaman tuberías, y maneja todo   el trabajo encubierto requerido por el   computación. [3] El enlace y el retorno   operadores intercalados en la tubería   se ejecutará después de cada monádica   la función devuelve el control, y lo hará   cuidar los aspectos particulares   manejado por la mónada.

Creo que lo explica muy bien.


10
2017-12-01 03:58



Trataré de hacer la definición más corta que puedo administrar 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 de 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 lista de mónadas 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. los flatMap la función es muy parecida a la más común map. Recibe un argumento de función (también conocido como delegado), 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 transferida también ajuste su valor de retorno en el mismo tipo de clase genérica. Para ayudar con eso, proporciona create, un constructor que puede crear una instancia de esa clase genérica a partir de un valor. El resultado de retorno de flatMap es también una clase genérica del mismo tipo, a menudo empaquetando los mismos valores que estaban 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())

Resulta que este tipo de clase genérica es útil como modelo base para una gran cantidad de cosas. Esto (junto con los jergaismos 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 están especializados.

Por ejemplo, puede modelar excepciones utilizando contenedores monádicos. Cada contenedor contendrá el resultado de la operación o el error que se haya producido. La siguiente función (delegar) en la cadena de devoluciones de llamada de flatMap solo se invocará si la anterior contiene un valor en el contenedor. De lo contrario, si se empaqueta 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 manejador de errores conectada mediante un método llamado .orElse() (tal método sería una extensión permitida)

Notas: Los lenguajes funcionales le permiten escribir funciones que pueden operar en cualquier tipo de clase genérica monádica. Para que esto funcione, uno debería escribir una interfaz genérica para mónadas. No sé si es posible escribir una interfaz de este tipo en C #, pero hasta donde yo sé, no 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
}

8
2018-02-16 16:11