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.
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 null
tan 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 null
por todo el asunto. Sería tedioso tener que verificar manualmente cada búsqueda
null
y 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é
Bind
má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 Nullable
en 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 Bind
función para vincular una variable al contenido de nuestro Nullable
valor 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
, g
o h
devuelve null) o será el resultado de sumar f
, g
yh
juntos. (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 Bind
operador 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
volver 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 Nullable
estructura al lambda.
Aquí está el Bind
operador:
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 a
a
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 Bind
operador manejará toda la lógica de verificación nula por nosotros. Si y solo si el valor al que llamamos
Bind
no 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 Bind
y obtener una variable ligada al valor dentro del valor monádico ( fval
,
gval
y hval
en el código de ejemplo) y que les puede utilizar segura en el conocimiento que Bind
se 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 Bind
operador 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 Bind
resuelve 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 Bind
operador, 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 do
notació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 do
notación, lo que significa que básicamente puede escribir sus propios pequeños idiomas simplemente definiendo dos funciones.