Lo primero que debe saber acerca de la mónada de continuación es que, fundamentalmente, no es realmente haciendo nada en absoluto. ¡Es verdad!
La idea básica de una continuación, en general, es que representa el resto de un cálculo. Digamos que tenemos una expresión como esta: foo (bar x y) z
. Ahora, extraiga solo la parte entre paréntesis, bar x y
--esto es parte de la expresión total, pero no es solo una función que podemos aplicar. En cambio, es algo que necesitamos para aplicar una función a. Entonces, podemos hablar del "resto del cálculo" en este caso como \a -> foo a z
, que podemos aplicar al bar x y
para reconstruir el formulario completo.
Ahora, sucede que este concepto de "el resto de la computación" es útil, pero es incómodo trabajar con él, ya que es algo fuera de la subexpresión que estamos considerando. Para que las cosas funcionen mejor, podemos cambiar las cosas al revés: extraemos la subexpresión que nos interesa y luego la envolvemos en una función que toma un argumento que representa el resto del cálculo: \k -> k (bar x y)
.
Esta versión modificada nos da mucha flexibilidad, no solo extrae una subexpresión de su contexto, sino que nos permite manipular ese contexto externo dentro de la subexpresión misma. Podemos considerarlo como una especie de cómputo suspendido, que nos da un control explícito sobre lo que sucede a continuación. Ahora, ¿cómo podríamos generalizar esto? Bueno, la subexpresión prácticamente no se modifica, así que simplemente reemplácela con un parámetro para la función de adentro hacia afuera, dándonos \x k -> k x
--en otras palabras, nada más que la aplicación de función , invertida. Podríamos simplemente escribir flip ($)
, o agregar un poco de un sabor de idioma extranjero exótico y definirlo como un operador |>
.
Ahora, sería simple, aunque tedioso y horriblemente ofuscante, traducir cada parte de una expresión a esta forma. Afortunadamente, hay una mejor manera. Como programadores de Haskell, cuando pensamos en construyendo un cálculo dentro de un contexto de contexto, lo siguiente que pensamos es decir, ¿es esto una mónada? Y en este caso la respuesta es sí, sí lo es.
de convertir esto en una mónada, comenzamos con dos bloques de construcción básicos:
- Para una mónada
m
, un valor de tipo m a
representa tener acceso a un valor de tipo a
en el contexto de la mónada .
- El núcleo de nuestros "cálculos suspendidos" es la aplicación de función volteada.
¿Qué significa tener acceso a algo del tipo a
dentro de este contexto?Simplemente significa que, para algún valor x :: a
, hemos aplicado flip ($)
a x
, dándonos una función que toma una función que toma un argumento del tipo a
, y aplica esa función a x
. Digamos que tenemos un cálculo suspendido que contiene un valor de tipo Bool
. ¿Qué tipo nos da esto?
> :t flip ($) True
flip ($) True :: (Bool -> b) -> b
Así que para los cálculos en suspensión, el tipo m a
se resuelve a (a -> b) -> b
... que es tal vez una decepción, puesto que ya se sabía la firma de Cont
, pero sígueme por ahora.
Una cosa interesante a destacar aquí es que una especie de "inversión" también se aplica al tipo del mismo mónada: Cont b a
representa una función que toma una función a -> b
y evalúa a b
. Como una continuación representa "el futuro" de un cálculo, entonces el tipo a
en la firma representa en cierto sentido "el pasado".
Entonces, reemplazando (a -> b) -> b
con Cont b a
, ¿cuál es el tipo monádico para nuestra aplicación básica de bloque de construcción de función inversa? a -> (a -> b) -> b
se traduce en a -> Cont b a
... el mismo tipo de firma como return
y, de hecho, eso es exactamente lo que es.
A partir de ahora, todo se cae directamente de los tipos: esencialmente no hay una forma sensata de implementar >>=
además de la implementación real. ¿Pero qué es realmente haciendo?
En este punto volvemos a lo que dije inicialmente: la mónada de continuación no es realmente haciendo mucho más. Algo del tipo Cont r a
es trivialmente equivalente a algo del tipo a
, simplemente suministrando id
como argumento para el cálculo suspendido. Esto podría llevar a preguntarse si, en caso Cont r a
es una monada, pero la conversión es tan trivial, no debería a
sola también ser una mónada? Por supuesto, eso no funciona como está, ya que no hay un constructor de tipos para definir como una instancia de Monad
, pero digamos que agregamos un contenedor trivial, como data Id a = Id a
. Esto es de hecho una mónada, a saber, la mónada de identidad.
¿Qué hace >>=
para la mónada identidad? La firma de tipo es Id a -> (a -> Id b) -> Id b
, que es equivalente a a -> (a -> b) -> b
, que es simplemente una aplicación de función simple otra vez. Una vez establecido que Cont r a
es trivialmente equivalentes a Id a
, se puede deducir que en este caso también, (>>=)
es la aplicación simplemente función.
Por supuesto, Cont r a
es un mundo invertido loca donde todo el mundo tiene barbas de chivo, así que lo que realmente sucede implica cosas arrastrando los pies alrededor de formas confusas con el fin de cálculos cadena de dos suspendidos juntos en un nuevo cómputo suspendido, pero en esencia, no ISN ¡En realidad no pasa nada inusual! Aplicando funciones a argumentos, ho hum, otro día en la vida de un programador funcional.
¿Está familiarizado con CPS? Si no, deberías buscar tutoriales sobre eso (no sé ninguno), porque haría mucho más fácil Cont. –