En C #, ¿Qué es una mónada?


190

Se habla mucho de mónadas en estos días. He leído algunos artículos / publicaciones de blog, pero no puedo ir lo suficientemente lejos con sus ejemplos para comprender completamente el concepto. La razón es que las mónadas son un concepto de lenguaje funcional y, por lo tanto, los ejemplos están en idiomas con los que no he trabajado (ya que no he usado un lenguaje funcional en profundidad). No puedo comprender la sintaxis lo suficientemente profundo como para seguir los artículos por completo ... pero puedo decir que hay algo que vale la pena entender allí.

Sin embargo, conozco C # bastante bien, incluidas las expresiones lambda y otras características funcionales. Sé que C # solo tiene un subconjunto de características funcionales, por lo que tal vez las mónadas no se puedan expresar en C #.

Sin embargo, ¿seguramente es posible transmitir el concepto? Por lo menos eso espero. Tal vez pueda presentar un ejemplo de C # como base y luego describir lo que un desarrollador de C # desearía poder hacer desde allí, pero no puede porque el lenguaje carece de funciones de programación funcionales. Esto sería fantástico, ya que transmitiría la intención y los beneficios de las mónadas. Así que aquí está mi pregunta: ¿Cuál es la mejor explicación que puede dar de mónadas a un desarrollador C # 3?

¡Gracias!

(EDITAR: Por cierto, sé que ya hay al menos 3 preguntas de "qué es una mónada" en SO. Sin embargo, me enfrento al mismo problema con ellas ... por lo que esta pregunta es necesaria en mi opinión, debido al desarrollador de C # enfoque. Gracias.)


Tenga en cuenta que en realidad es un desarrollador de C # 3.0. No lo confundas con .NET 3.5. Aparte de eso, buena pregunta.
Razzie

44
Vale la pena señalar que las expresiones de consulta LINQ son un ejemplo de comportamiento monádico en C # 3.
Erik Forbes

1
Sigo pensando que es una pregunta duplicada. Una de las respuestas en stackoverflow.com/questions/2366/can-anyone-explain-monads enlaza con channel9vip.orcsweb.com/shows/Going+Deep/… , donde uno de los comentarios tiene un muy buen ejemplo de C #. :)
jalf

44
Aún así, ese es solo un enlace de una respuesta a una de las preguntas SO. Veo valor en una pregunta centrada en desarrolladores de C #. Es algo que le preguntaría a un programador funcional que solía hacer C # si conociera uno, por lo que parece razonable preguntarlo en SO. Pero también respeto tu derecho a tu opinión.
Charlie Flowers

1
¿No es una respuesta todo lo que necesitas? ;) Mi punto es solo que una de las otras preguntas (y ahora también esta, así que sí) tenía una respuesta específica de C # (que parece muy bien escrita, en realidad. Probablemente la mejor explicación que he visto)
jalf

Respuestas:


147

La mayor parte de lo que hace en la programación durante todo el día es combinar algunas funciones para crear funciones más grandes a partir de ellas. Por lo general, no solo tiene funciones en su caja de herramientas, sino también otras cosas como operadores, asignaciones de variables y similares, pero en general su programa combina muchos "cálculos" con cálculos más grandes que se combinarán aún más.

Una mónada es alguna forma de hacer esta "combinación de cálculos".

Por lo general, su "operador" más básico para combinar dos cálculos es ;:

a; b

Cuando dices esto quieres decir "primero haz a, luego haz b". El resultado a; bes básicamente una vez más un cálculo que se puede combinar junto con más cosas. Esta es una mónada simple, es una forma de combinar pequeños cálculos con los más grandes. El ;dice "haz lo de la izquierda, luego haz lo de la derecha".

Otra cosa que puede verse como una mónada en lenguajes orientados a objetos es el .. A menudo encuentras cosas como esta:

a.b().c().d()

Los .básicamente significa "evalúan el cálculo de la izquierda, y luego llamar al método a la derecha en el resultado de eso". Es otra forma de combinar funciones / cálculos, un poco más complicado que ;. Y el concepto de encadenar cosas .es una mónada, ya que es una forma de combinar dos cálculos en un nuevo cálculo.

Otra mónada bastante común, que no tiene una sintaxis especial, es este patrón:

rv = socket.bind(address, port);
if (rv == -1)
  return -1;

rv = socket.connect(...);
if (rv == -1)
  return -1;

rv = socket.send(...);
if (rv == -1)
  return -1;

Un valor de retorno de -1 indica falla, pero no hay una forma real de abstraer esta comprobación de errores, incluso si tiene muchas llamadas API que necesita combinar de esta manera. Básicamente, esta es otra mónada que combina las llamadas de función por la regla "si la función de la izquierda devolvió -1, devuelve -1 nosotros mismos, de lo contrario, llame a la función de la derecha". Si tuviéramos un operador >>=que hiciera esto, simplemente podríamos escribir:

socket.bind(...) >>= socket.connect(...) >>= socket.send(...)

Haría las cosas más legibles y ayudaría a resumir nuestra forma especial de combinar funciones, de modo que no necesitemos repetirnos una y otra vez.

Y hay muchas más formas de combinar funciones / cálculos que son útiles como un patrón general y que pueden resumirse en una mónada, lo que permite al usuario de la mónada escribir código mucho más conciso y claro, ya que toda la contabilidad y administración de Las funciones utilizadas se realizan en la mónada.

Por ejemplo, lo anterior >>=podría extenderse para "hacer la verificación de errores y luego llamar al lado derecho en el socket que obtuvimos como entrada", de modo que no necesitemos especificar explícitamente socketmuchas veces:

new socket() >>= bind(...) >>= connect(...) >>= send(...);

La definición formal es un poco más complicada ya que debe preocuparse por cómo obtener el resultado de una función como entrada a la siguiente, si esa función necesita esa entrada y ya que desea asegurarse de que las funciones que combina encajan la forma en que intentas combinarlos en tu mónada. Pero el concepto básico es que formaliza diferentes formas de combinar funciones.


28
¡Gran respuesta! Voy a incluir una cita de Oliver Steele, tratando de relacionar las mónadas con la sobrecarga del operador a la C ++ o C #: las mónadas le permiten sobrecargar el ';' operador.
Jörg W Mittag el

66
@ JörgWMittag Leí esa cita antes, pero sonaba como una tontería demasiado embriagadora. Ahora que entiendo las mónadas y leo esta explicación de cómo ';' es uno, lo entiendo. Pero creo que es realmente una declaración irracional para la mayoría de los desarrolladores imperativos. ';' no se ve como un operador más de lo que // es para la mayoría.
Jimmy Hoffa el

2
¿Estás seguro de que sabes lo que es una mónada? Mónadas no es una "función" o cálculo, hay reglas para las mónadas.
Luis

En su ;ejemplo: ¿Qué objetos / tipos de datos asigna ;? (Piense en Listmapas Tpara List<T>) ¿Cómo funciona el ;mapa de morfismos / funciones entre objetos / tipos de datos? Lo que es pure, join, bindpara ;?
Micha Wiedenmann

44

Ha pasado un año desde que publiqué esta pregunta. Después de publicarlo, profundicé en Haskell durante un par de meses. Lo disfruté muchísimo, pero lo dejé a un lado justo cuando estaba listo para profundizar en las mónadas. Regresé al trabajo y me concentré en las tecnologías que requería mi proyecto.

Y anoche, vine y releí estas respuestas. Lo que es más importante , releí el ejemplo específico de C # en los comentarios de texto del video de Brian Beckman que alguien menciona arriba . Fue tan claro e iluminador que decidí publicarlo directamente aquí.

Debido a este comentario, no solo siento que entiendo exactamente qué son las mónadas ... me doy cuenta de que en realidad escribí algunas cosas en C # que son mónadas ... o al menos muy cercanas, y me esfuerzo por resolver los mismos problemas.

Entonces, aquí está el comentario: esta es una cita directa del comentario aquí por sylvan :

Esto está muy bien. Sin embargo, es un poco abstracto. Me imagino que las personas que no saben qué mónadas ya están confundidas debido a la falta de ejemplos reales.

Así que déjame intentar cumplir, y para ser realmente claro, haré un ejemplo en C #, aunque se verá feo. Agregaré el Haskell equivalente al final y le mostraré el azúcar sintáctico genial Haskell que es donde, en mi opinión, las mónadas realmente comienzan a ser útiles.

Bien, una de las mónadas más fáciles se llama "Quizás mónada" en Haskell. En C # se llama al tipo Quizás Nullable<T>. Básicamente es una clase pequeña que simplemente encapsula el concepto de un valor que es válido y tiene un valor, o es "nulo" y no tiene ningún valor.

Una cosa útil para quedarse dentro de una mónada para combinar valores de este tipo es la noción de falla. Es decir, queremos poder ver múltiples valores anulables y regresar nulltan pronto como cualquiera de ellos sea nulo. Esto podría ser útil si, por ejemplo, busca muchas claves en un diccionario o algo así, y al final desea procesar todos los resultados y combinarlos de alguna manera, pero si alguna de las claves no está en el diccionario, quieres volver nullpor todo el asunto. Sería tedioso tener que verificar manualmente cada búsqueda nully devolución, por lo que podemos ocultar esta comprobación dentro del operador de enlace (que es una especie de punto de mónadas, ocultamos la contabilidad en el operador de enlace que hace que el código sea más fácil de leer). usar ya que podemos olvidarnos de los detalles).

Aquí está el programa que motiva todo (definiré Bindmás adelante, esto es solo para mostrarle por qué es bueno).

 class Program
    {
        static Nullable<int> f(){ return 4; }        
        static Nullable<int> g(){ return 7; }
        static Nullable<int> h(){ return 9; }


        static void Main(string[] args)
        {
            Nullable<int> z = 
                        f().Bind( fval => 
                            g().Bind( gval => 
                                h().Bind( hval =>
                                    new Nullable<int>( fval + gval + hval ))));

            Console.WriteLine(
                    "z = {0}", z.HasValue ? z.Value.ToString() : "null" );
            Console.WriteLine("Press any key to continue...");
            Console.ReadKey();
        }
    }

Ahora, ignore por un momento que ya hay soporte para hacer esto Nullableen C # (puede agregar entradas anulables juntas y obtendrá nulo si cualquiera es nulo). Supongamos que no existe tal característica, y es solo una clase definida por el usuario sin magia especial. El punto es que podemos usar la Bindfunción para vincular una variable al contenido de nuestro Nullablevalor y luego pretender que no está sucediendo nada extraño, y usarlos como entradas normales y simplemente sumarlos. Hemos terminado el resultado en un anulable al final, y que sea anulable a ser nulo (si alguno de f, go hdevuelve null) o será el resultado de sumar f, gyhjuntos. (esto es análogo a cómo podemos vincular una fila en una base de datos a una variable en LINQ, y hacer cosas con ella, sabiendo que el Bindoperador se asegurará de que la variable solo pase valores de fila válidos).

Puedes jugar con esto y cambiar cualquiera de f, gy hvolver nulo y verás que todo volverá nulo.

Entonces, claramente, el operador de vinculación tiene que hacer esta verificación por nosotros y rescatar a null si encuentra un valor nulo, y de lo contrario pasar el valor dentro de la Nullableestructura al lambda.

Aquí está el Bindoperador:

public static Nullable<B> Bind<A,B>( this Nullable<A> a, Func<A,Nullable<B>> f ) 
    where B : struct 
    where A : struct
{
    return a.HasValue ? f(a.Value) : null;
}

Los tipos aquí son como en el video. Toma una M a ( Nullable<A>en la sintaxis de C # para este caso), y una función de aa M b( Func<A, Nullable<B>>en la sintaxis de C #), y devuelve un M b ( Nullable<B>).

El código simplemente verifica si el nulo contiene un valor y si es así lo extrae y lo pasa a la función, de lo contrario, simplemente devuelve nulo. Esto significa que el Bindoperador manejará toda la lógica de verificación nula por nosotros. Si y solo si el valor al que llamamos Bindno es nulo, ese valor se "pasará" a la función lambda, de lo contrario saldremos temprano y la expresión completa será nula. Esto permite que el código que escriben haciendo uso de la mónada ser totalmente libre de este comportamiento nulo de cheques, sólo tiene que utilizar Bindy obtener una variable ligada al valor dentro del valor monádico ( fval, gvaly hvalen el código de ejemplo) y que les puede utilizar segura en el conocimiento que Bindse encargará de verificar si son nulos antes de pasarlos.

Hay otros ejemplos de cosas que puedes hacer con una mónada. Por ejemplo, puede hacer que el Bindoperador se encargue de una secuencia de entrada de caracteres y usarla para escribir combinadores de analizador sintáctico. Cada combinador de analizador puede ser completamente ajeno a cosas como el seguimiento de retroceso, fallas de analizador, etc., y simplemente combinar analizadores más pequeños como si las cosas nunca salieran mal, con la seguridad de que una implementación inteligente Bindresuelve toda la lógica detrás del partes difíciles Luego, tal vez alguien agregue el registro a la mónada, pero el código que usa la mónada no cambia, porque toda la magia ocurre en la definición del Bindoperador, el resto del código no cambia.

Finalmente, aquí está la implementación del mismo código en Haskell ( -- comienza una línea de comentarios).

-- Here's the data type, it's either nothing, or "Just" a value
-- this is in the standard library
data Maybe a = Nothing | Just a

-- The bind operator for Nothing
Nothing >>= f = Nothing
-- The bind operator for Just x
Just x >>= f = f x

-- the "unit", called "return"
return = Just

-- The sample code using the lambda syntax
-- that Brian showed
z = f >>= ( \fval ->
     g >>= ( \gval ->  
     h >>= ( \hval -> return (fval+gval+hval ) ) ) )

-- The following is exactly the same as the three lines above
z2 = do 
   fval <- f
   gval <- g
   hval <- h
   return (fval+gval+hval)

Como puede ver, la agradable donotación al final hace que parezca un código imperativo directo. Y de hecho esto es por diseño. Las mónadas se pueden usar para encapsular todas las cosas útiles en la programación imperativa (estado mutable, IO, etc.) y se usan con esta buena sintaxis imperativa, pero detrás de las cortinas, ¡todo son solo mónadas y una implementación inteligente del operador de enlace! Lo bueno es que puedes implementar tus propias mónadas implementando >>=y return. Y si lo hace, esas mónadas también podrán usar la donotación, lo que significa que básicamente puede escribir sus propios pequeños idiomas simplemente definiendo dos funciones.


3
Personalmente, prefiero la versión de F # de la mónada, pero en cualquier caso son increíbles.
ChaosPandion

3
Gracias por volver aquí y actualizar tu publicación. Son seguimientos como estos los que ayudan a los programadores que están investigando un área específica a comprender realmente cómo los programadores compañeros consideran en última instancia dicha área, en lugar de simplemente tener "cómo hacer la tecnología x in y". ¡Eres el hombre!
kappasims

Básicamente, tomé el mismo camino que tú y llegué al mismo lugar entendiendo a las mónadas, que dice que esta es la mejor explicación del comportamiento vinculante de una mónada que he visto para un desarrollador imperativo. Aunque creo que no estás tocando todo acerca de las mónadas, lo que se explica un poco más arriba con algo.
Jimmy Hoffa el

@Jimmy Hoffa: sin duda tienes razón. Creo que realmente los entiendo más profundamente, la mejor manera es comenzar a usarlos mucho y obtener experiencia . Todavía no he tenido esa oportunidad, pero espero tenerla pronto.
Charlie Flowers

Parece que mónada es solo un nivel más alto de abstracción en términos de programación, o es solo una definición de función continua y no diferenciable en matemáticas. De cualquier manera, no son un concepto nuevo, especialmente en matemáticas.
liang

11

Una mónada es esencialmente procesamiento diferido. Si está intentando escribir código que tiene efectos secundarios (p. Ej., E / S) en un idioma que no los permite y solo permite la computación pura, una elusión es decir: "Ok, sé que no hará efectos secundarios para mí, pero ¿puedes calcular qué pasaría si lo hicieras? "

Es una especie de trampa.

Ahora, esa explicación te ayudará a comprender la intención general de las mónadas, pero el diablo está en los detalles. ¿Cómo funciona exactamente qué a calcular las consecuencias? A veces no es bonito.

La mejor manera de dar una visión general de cómo para alguien acostumbrado a la programación imperativa es decir que lo coloca en un DSL en el que las operaciones que se parecen sintácticamente a lo que está acostumbrado fuera de la mónada se utilizan en su lugar para construir una función que sirva lo que desea si pudiera (por ejemplo) escribir en un archivo de salida. Casi (pero no realmente) como si estuviera construyendo código en una cadena para luego ser evaluado.


1
¿Como en el libro I Robot? ¿Dónde el científico le pide a una computadora que calcule los viajes espaciales y les pide que omitan ciertas reglas? :) :) :) :)
OscarRyz el

3
Hmm, A Monad se puede usar para procesamiento diferido y para encapsular funciones de efectos secundarios, de hecho esa fue su primera aplicación real en Haskell, pero en realidad es un patrón mucho más genérico. Otros usos comunes incluyen el manejo de errores y la gestión del estado. El azúcar sintáctico (hacer en Haskell, expresiones de cálculo en F #, sintaxis de Linq en C #) es simplemente eso y fundamental para las mónadas como tal.
Mike Hadlow

@MikeHadlow: Las instancias de mónada para el manejo de errores ( Maybey Either e) y la administración de estado ( State s, ST s) me parecen instancias particulares de "Por favor calcule qué sucedería si lo hiciera [efectos secundarios para mí]". Otro ejemplo sería el no determinismo ( []).
pyon

esto es exactamente correcto; con una (bueno, dos) adiciones de que es un E DSL, es decir, un DSL incorporado , ya que cada valor "monádico" es un valor válido de su lenguaje "puro" en sí mismo, representando un "cálculo" potencialmente impuro. Además, existe una construcción monádica de "vinculación" en su lenguaje puro que le permite encadenar constructores puros de tales valores donde cada uno será invocado con el resultado de su cálculo anterior, cuando todo el cálculo combinado se "ejecute". Esto significa que tenemos la capacidad de ramificarnos en resultados futuros (o, en cualquier caso, de la línea de tiempo "ejecutar" por separado).
Will Ness

pero para un programador significa que podemos programar en el EDSL mientras lo mezclamos con los cálculos puros de nuestro lenguaje puro. una pila de sándwiches de capas múltiples es un sándwich de capas múltiples. Es así de simple.
Will Ness

4

Estoy seguro de que otros usuarios publicarán en profundidad, pero encontré este video útil hasta cierto punto, pero diré que todavía no estoy al punto de fluidez con el concepto de tal manera que podría (o debería) comenzar a resolver problemas intuitivamente con las mónadas.


1
Lo que encontré aún más útil fue el comentario que contenía un ejemplo de C # debajo del video.
jalf

No sé más útil, pero ciertamente puso en práctica las ideas.
TheMissingLINQ

0

Puedes pensar en una mónada como un C # interfaceque las clases tienen que implementar . Esta es una respuesta pragmática que ignora toda la categoría de matemática teórica detrás de por qué querría elegir tener estas declaraciones en su interfaz e ignora todas las razones por las que desea tener mónadas en un lenguaje que intenta evitar efectos secundarios, pero me pareció un buen comienzo como alguien que entiende las interfaces (C #).


¿Puedes elaborar? ¿De qué se trata una interfaz que lo relaciona con las mónadas?
Joel Coehoorn el

2
Creo que la publicación del blog gasta varios párrafos dedicados a esa pregunta.
hao

0

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 del pseudocódigo mónada. (Como referencia: Mes el constructor de tipos, feedes la operación "vincular" y wrapes la operación "regresar").

using System.IO;
using System;

class Program
{
    public class M<A>
    {
        public A val;
        public string messages;
    }

    public static M<B> feed<A, B>(Func<A, M<B>> f, M<A> x)
    {
        M<B> m = f(x.val);
        m.messages = x.messages + m.messages;
        return m;
    }

    public static M<A> wrap<A>(A x)
    {
        M<A> m = new M<A>();
        m.val = x;
        m.messages = "";
        return m;
    }

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

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

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

    static void Main()
    {
        V x = new V();
        M<T> m = feed<U, T>(f, feed(g, wrap<V>(x)));
        Console.Write(m.messages);
    }
}
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.