Como siempre, la terminología que usa la gente no es del todo coherente. Hay una variedad de nociones inspiradas en mónadas, pero estrictamente hablando, no lo son. El término "mónada indexada" es uno de un número (incluyendo "mónada" y "mónada parametrizada" (el nombre de Atkey para ellos)) de términos usados para caracterizar una de tales nociones. (Otra noción de este tipo, si está interesado, es la "mónada de efecto paramétrico" de Katsumata, indexada por un monoide, donde el retorno se indexa de forma neutral y la vinculación se acumula en su índice).
En primer lugar, veamos los tipos.
IxMonad (m :: state -> state -> * -> *)
Es decir, el tipo de "cálculo" (o "acción", si lo prefiere, pero me quedo con "cálculo"), parece
m before after value
donde before, after :: state
y value :: *
. La idea es capturar los medios para interactuar de forma segura con un sistema externo que tiene una noción predecible de estado. El tipo de cálculo le dice cuál debe ser el estado en el que before
se ejecuta, cuál será el estado en el que after
se ejecuta y (como con las mónadas regulares *
) qué tipo de value
s produce el cálculo.
Las partes y piezas habituales son, a la vez, *
como una mónada y, a la vez, state
como jugar al dominó.
ireturn :: a -> m i i a -- returning a pure value preserves state
ibind :: m i j a -> -- we can go from i to j and get an a, thence
(a -> m j k b) -- we can go from j to k and get a b, therefore
-> m i k b -- we can indeed go from i to k and get a b
La noción de "flecha de Kleisli" (función que produce cálculo) así generada es
a -> m i j b -- values a in, b out; state transition i to j
y obtenemos una composición
icomp :: IxMonad m => (b -> m j k c) -> (a -> m i j b) -> a -> m i k c
icomp f g = \ a -> ibind (g a) f
y, como siempre, las leyes garantizan exactamente eso ireturn
y icomp
nos dan una categoría
ireturn `icomp` g = g
f `icomp` ireturn = f
(f `icomp` g) `icomp` h = f `icomp` (g `icomp` h)
o, en la comedia falsa C / Java / lo que sea,
g(); skip = g()
skip; f() = f()
{g(); h()}; f() = h(); {g(); f()}
¿Por qué molestarse? Modelar "reglas" de interacción. Por ejemplo, no puede expulsar un DVD si no hay uno en la unidad y no puede colocar un DVD en la unidad si ya hay uno. Entonces
data DVDDrive :: Bool -> Bool -> * -> * where -- Bool is "drive full?"
DReturn :: a -> DVDDrive i i a
DInsert :: DVD -> -- you have a DVD
DVDDrive True k a -> -- you know how to continue full
DVDDrive False k a -- so you can insert from empty
DEject :: (DVD -> -- once you receive a DVD
DVDDrive False k a) -> -- you know how to continue empty
DVDDrive True k a -- so you can eject when full
instance IxMonad DVDDrive where -- put these methods where they need to go
ireturn = DReturn -- so this goes somewhere else
ibind (DReturn a) k = k a
ibind (DInsert dvd j) k = DInsert dvd (ibind j k)
ibind (DEject j) k = DEject j $ \ dvd -> ibind (j dvd) k
Con esto en su lugar, podemos definir los comandos "primitivos"
dInsert :: DVD -> DVDDrive False True ()
dInsert dvd = DInsert dvd $ DReturn ()
dEject :: DVDrive True False DVD
dEject = DEject $ \ dvd -> DReturn dvd
de los cuales se ensamblan otros con ireturn
y ibind
. Ahora, puedo escribir (tomar prestado una do
nota)
discSwap :: DVD -> DVDDrive True True DVD
discSwap dvd = do dvd' <- dEject; dInsert dvd ; ireturn dvd'
pero no lo físicamente imposible
discSwap :: DVD -> DVDDrive True True DVD
discSwap dvd = do dInsert dvd; dEject -- ouch!
Alternativamente, uno puede definir sus comandos primitivos directamente
data DVDCommand :: Bool -> Bool -> * -> * where
InsertC :: DVD -> DVDCommand False True ()
EjectC :: DVDCommand True False DVD
y luego instancia la plantilla genérica
data CommandIxMonad :: (state -> state -> * -> *) ->
state -> state -> * -> * where
CReturn :: a -> CommandIxMonad c i i a
(:?) :: c i j a -> (a -> CommandIxMonad c j k b) ->
CommandIxMonad c i k b
instance IxMonad (CommandIxMonad c) where
ireturn = CReturn
ibind (CReturn a) k = k a
ibind (c :? j) k = c :? \ a -> ibind (j a) k
En efecto, hemos dicho cuáles son las flechas primitivas de Kleisli (qué es un "dominó"), y luego hemos construido una noción adecuada de "secuencia de cálculo" sobre ellas.
Tenga en cuenta que para cada mónada indexada m
, la "diagonal sin cambios" m i i
es una mónada, pero en general m i j
no lo es. Además, los valores no están indexados, pero los cálculos están indexados, por lo que una mónada indexada no es solo la idea habitual de una mónada instanciada para alguna otra categoría.
Ahora, mire nuevamente el tipo de flecha de Kleisli
a -> m i j b
Sabemos que debemos estar en estado i
para comenzar, y predecimos que cualquier continuación comenzará desde el estado j
. ¡Sabemos mucho sobre este sistema! ¡Esta no es una operación arriesgada! Cuando ponemos el DVD en la unidad, ¡entra! La unidad de DVD no tiene nada que decir sobre el estado después de cada comando.
Pero eso no es cierto en general, al interactuar con el mundo. A veces, es posible que deba ceder algo de control y dejar que el mundo haga lo que quiera. Por ejemplo, si es un servidor, puede ofrecerle a su cliente una opción y el estado de su sesión dependerá de lo que elija. La operación de "elección de oferta" del servidor no determina el estado resultante, pero el servidor debería poder continuar de todos modos. No es un "comando primitivo" en el sentido anterior, por lo que las mónadas indexadas no son una herramienta tan buena para modelar el escenario impredecible .
¿Qué es una herramienta mejor?
type f :-> g = forall state. f state -> g state
class MonadIx (m :: (state -> *) -> (state -> *)) where
returnIx :: x :-> m x
flipBindIx :: (a :-> m b) -> (m a :-> m b) -- tidier than bindIx
¿Galletas de miedo? Realmente no, por dos razones. Primero, se parece más a lo que es una mónada, porque es una mónada, pero más (state -> *)
bien que *
. Dos, si miras el tipo de flecha Kleisli,
a :-> m b = forall state. a state -> m b state
obtienes el tipo de cálculos con una condición previa a
y una condición posterior b
, como en Good Old Hoare Logic. Las afirmaciones en la lógica del programa han tardado menos de medio siglo en cruzar la correspondencia Curry-Howard y convertirse en tipos Haskell. El tipo de returnIx
dice "puede lograr cualquier condición posterior que se cumpla, simplemente sin hacer nada", que es la regla de Hoare Logic para "omitir". La composición correspondiente es la regla de Hoare Logic para ";".
Terminemos mirando el tipo de bindIx
, poniendo todos los cuantificadores.
bindIx :: forall i. m a i -> (forall j. a j -> m b j) -> m b i
Estos forall
s tienen polaridad opuesta. Elegimos el estado inicial i
y un cálculo que puede comenzar en i
, con poscondición a
. El mundo elige cualquier estado intermedio j
que le guste, pero debe darnos la evidencia de que la poscondición b
es válida y, a partir de ese estado, podemos continuar para b
mantenernos. Entonces, en secuencia, podemos lograr la condición b
del estado i
. Al soltar nuestro control sobre los estados "posteriores", podemos modelar cálculos impredecibles .
Ambos IxMonad
y MonadIx
son útiles. Ambos modelan la validez de los cálculos interactivos con respecto al estado cambiante, predecible e impredecible, respectivamente. La previsibilidad es valiosa cuando se puede obtener, pero la imprevisibilidad a veces es una realidad. Con suerte, entonces, esta respuesta da alguna indicación de qué son las mónadas indexadas, prediciendo cuándo comienzan a ser útiles y cuándo se detienen.
True
/False
como argumentos de tipoDVDDrive
? ¿Es una extensión o los valores booleanos se escriben aquí?