preestablecida - Ayude a un desarrollador de C#a comprender: ¿Qué es una mónada?
mónadas (6)
Estoy seguro de que otros usuarios publicarán en profundidad, pero encontré que este video fue útil hasta cierto punto, pero diré que aún no llego al punto de fluidez con el concepto, de modo que podría (o debería) comenzar a resolver problemas intuitivamente con las Mónadas.
Se habla mucho sobre las mónadas en estos días. He leído algunos artículos / publicaciones en blogs, 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 (dado que no he usado un lenguaje funcional en profundidad). No puedo entender la sintaxis lo suficientemente profundo como para seguir los artículos completamente ... pero puedo decir que hay algo que vale la pena entender.
Sin embargo, conozco bastante bien C #, incluidas las expresiones lambda y otras características funcionales. Sé que C # solo tiene un subconjunto de características funcionales, por lo que quizás 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 hacerlo porque el lenguaje carece de funciones de programación funcional. Esto sería fantástico, porque 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 las mónadas a un desarrollador de C # 3?
¡Gracias!
(EDITAR: Por cierto, sé que ya hay al menos 3 preguntas "¿Qué es una mónada?" En SO. Sin embargo, me enfrento al mismo problema con ellas ... por lo que esta pregunta es necesaria por el desarrollador de C # enfoque. Gracias.)
Ha pasado un año desde que publiqué esta pregunta. Después de publicarlo, profundicé en Haskell por un par de meses. Lo disfruté muchísimo, pero lo dejé de lado justo cuando estaba listo para profundizar en las Mónadas. Volví al trabajo y me centré en las tecnologías que requería mi proyecto.
Y anoche, vine y volví a leer estas respuestas. Lo más importante es que volví a leer el ejemplo específico de C # en los comentarios de texto del video de Brian Beckman que alguien menciona arriba . Fue tan claro y esclarecedor 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 realmente he escrito algunas cosas en C # que son mónadas ... o al menos muy cercanas, y que se esfuerzan por resolver los mismos problemas.
Entonces, aquí está el comentario: esto es todo una cita directa del comentario de sylvan :
Esto está muy bien. Aunque es un poco abstracto Puedo imaginar personas que no saben qué mónadas ya se confunden 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 vea feo. Agregaré el Haskell equivalente al final y te mostraré el fantástico azúcar sintáctico Haskell, que es donde, IMO, las mónadas realmente comienzan a ser útiles.
Bien, entonces una de las Mónadas más fáciles se llama "Quizás mónada" en Haskell. En C #, el tipo Maybe se llama 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 pegar 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 devolver null
tan pronto como alguno 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 devolver null
para todo. Sería tedioso tener que verificar manualmente cada búsqueda null
y de regreso, para poder ocultar esta comprobación dentro del operador de vinculación (que es una especie de punto de mónadas, ocultamos la contabilidad en el operador de vinculación que facilita el código) para usar ya que podemos olvidarnos de los detalles).
Este es el programa que motiva todo ( Bind
el Bind
más adelante, esto es solo para mostrarte 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 para Nullable
en C # (puede agregar Nullable
que Nullable
nulos juntos y obtiene nulo si cualquiera de ellas es nulo). Imaginemos 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 función Bind
para vincular una variable a los contenidos de nuestro valor Nullable
y luego pretender que no pasa nada extraño, y usarlos como ints normales y simplemente sumarlos. Envolvemos el resultado en un valor nulo al final, y ese valor nulo será nulo (si alguno de f
, g
h
devuelve nulo) o será el resultado de sumar f
, g
h
juntos. (Esto es análogo a cómo podemos unir una fila en una base de datos a una variable en LINQ, y hacer cosas con ella, a sabiendas de que el operador Bind
se asegurará de que la variable solo pase valores de fila válidos).
Puedes jugar con esto y cambiar cualquiera de f
, g
, y h
para devolver nulo y verás que todo volverá nulo.
Entonces, claramente, el operador de enlace tiene que hacer esta comprobación para nosotros, y rescatar devolviendo nulo si encuentra un valor nulo, y de lo contrario pasar el valor dentro de la estructura Nullable
en el lambda.
Aquí está el operador Bind
:
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 sintaxis C # para este caso), y una función de a
a M b
( Func<A, Nullable<B>>
en sintaxis C #), y devuelve un M b
( Nullable<B>
).
El código simplemente verifica si el valor anulable contiene un valor y, si es así, lo extrae y lo pasa a la función, de lo contrario, devuelve nulo. Esto significa que el operador Bind
manejará toda la lógica de null-checking para nosotros. Si y solo si el valor al que llamamos Bind
no es nulo, ese valor se "transferirá" a la función lambda, de lo contrario saldremos pronto y toda la expresión será nula. Esto permite que el código que escribimos usando la mónada esté completamente libre de este comportamiento de verificación nula, solo usamos Bind
y obtenemos una variable vinculada al valor dentro del valor monádico ( fval
, gval
y hval
en el código de ejemplo) y puede usarlos de forma segura sabiendo que Bind
se encargará de verificar que no sean nulos antes de pasarlos.
Hay otros ejemplos de cosas que puedes hacer con una mónada. Por ejemplo, puede hacer que el operador Bind
se encargue de una secuencia de entrada de caracteres y la use para escribir los combinadores de analizadores. Cada combinador de analizadores puede entonces ser completamente ajeno a cosas como retrocesos, errores de analizador, etc., y simplemente combinar analizadores más pequeños juntos como si las cosas nunca salieran mal, a salvo sabiendo que una implementación inteligente de Bind
clasifica toda la lógica detrás los pedazos difíciles Luego, más tarde, tal vez alguien agrega el registro a la mónada, pero el código que utiliza la mónada no cambia, porque toda la magia ocurre en la definición del operador Bind
, el resto del código no se modifica.
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 buena notación do
hace que parezca un código imperativo directo. Y de hecho esto es por diseño. Las mónadas se pueden utilizar para encapsular todo el material útil en la programación imperativa (estado mutable, IO, etc.) y se utilizan con esta sintaxis similar a un imperativo, pero detrás de las cortinas, todo es solo mónadas y una implementación inteligente del operador bind. Lo bueno es que puedes implementar tus propias mónadas implementando >>=
y return
. Y si lo haces, esas mónadas también podrán usar la notación do
, lo que significa que básicamente puedes escribir tus propios pequeños lenguajes simplemente definiendo dos funciones.
Puedes pensar en una mónada como una interface
C # que las clases deben implementar . Esta es una respuesta pragmática que ignora toda la matemática teórica de categoría detrás de por qué desea 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 los efectos secundarios, pero me pareció un buen comienzo como alguien que entiende las interfaces (C #).
Una mónada es esencialmente procesamiento diferido. Si está intentando escribir un código que tiene efectos secundarios (por ejemplo, E / S) en un lenguaje que no lo permite, y solo permite el cálculo puro, una esquiva 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 entender la intención general de las mónadas, pero el diablo está en los detalles. ¿Cómo calcula exactamente las consecuencias? A veces, no es bonito.
La mejor manera de dar una visión general de cómo alguien acostumbrado a la programación imperativa es decir que lo pone en una DSL en donde las operaciones que parecen sintácticamente similares a las que está acostumbrado fuera de la mónada se usan en su lugar para construir una función que haría lo que quiere si pudiera (por ejemplo) escribir en un archivo de salida. Casi (pero no realmente) como si estuvieras construyendo código en una cadena para luego ser evaluado.
Ver mi answer 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".
Asume que no tiene conocimiento de programación funcional y utiliza pseudocódigo con function(argument) := expression
sintaxis de function(argument) := expression
con las expresiones más simples posibles.
Este programa C # es una implementación de la mónada de pseudocódigo. (Para referencia: M
es el constructor de tipo, la feed
es la operación de "vinculación", y el wrap
es la operación de "retorno").
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);
}
}
La mayor parte de lo que haces en programación todo el día es combinar algunas funciones juntas para crear funciones más grandes a partir de ellas. Usualmente no solo tiene funciones en su caja de herramientas sino también otras cosas como operadores, asignaciones de variables y similares, pero generalmente su programa combina muchos "cálculos" con cálculos más grandes que se combinarán más.
Una mónada es una 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
, haz b
". El resultado a; b
a; b
es básicamente una vez más un cálculo que se puede combinar con más cosas. Esta es una mónada simple, es una forma de combinar pequeños cálculos con otros más grandes. El ;
dice "haz lo de la izquierda, luego haz lo de la derecha".
Otra cosa que se puede ver como una mónada en los lenguajes orientados a objetos es el .
. A menudo encuentras cosas como esta:
a.b().c().d()
El .
básicamente significa "evaluar el cálculo de la izquierda, y luego llamar al método de la derecha sobre el resultado de eso". Es otra forma de combinar funciones / cálculos juntos, un poco más complicado que ;
. Y el concepto de encadenar cosas junto con .
es una mónada, ya que es una forma de combinar dos cálculos para 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. Esto es básicamente otra mónada que combina las llamadas de función con la regla "si la función de la izquierda devolvió -1, devuelve -1 nosotros mismos, de lo contrario llamamos 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 abstraer nuestra forma especial de combinar funciones, para que no tengamos que 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 pueden abstraerse en una mónada, lo que permite al usuario de la mónada escribir un 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, el anterior >>=
podría extenderse a "hacer la comprobación de errores y luego llamar al lado derecho del socket que obtuvimos como entrada", de modo que no necesitamos especificar explícitamente el socket
muchas veces:
new socket() >>= bind(...) >>= connect(...) >>= send(...);
La definición formal es un poco más complicada ya que tiene que preocuparse por cómo obtener el resultado de una función como entrada para la siguiente, si esa función necesita esa entrada y porque quiere asegurarse de que las funciones que combina se ajustan a la forma en que intentas combinarlos en tu mónada. Pero el concepto básico es que formalizas diferentes formas de combinar funciones juntas.