Comme toujours, la terminologie utilisée par les gens n'est pas entièrement cohérente. Il existe une variété de notions inspirées par les monades mais à proprement parler qui ne sont pas tout à fait. Le terme «monade indexée» est l'un d'un certain nombre (y compris «monade» et «monade paramétrée» (le nom d'Atkey pour eux)) de termes utilisés pour caractériser une telle notion. (Une autre notion de ce type, si vous êtes intéressé, est la "monade d'effet paramétrique" de Katsumata, indexée par un monoïde, où le retour est indexé de manière neutre et la liaison s'accumule dans son index.)
Tout d'abord, vérifions les types.
IxMonad (m :: state -> state -> * -> *)
Autrement dit, le type de "calcul" (ou "action", si vous préférez, mais je m'en tiendrai à "calcul"), ressemble à
m before after value
où before, after :: state
et value :: *
. L'idée est de saisir les moyens d'interagir en toute sécurité avec un système externe qui a une certaine notion prévisible d'état. Le type d'un calcul vous indique quel état doit être before
il s'exécute, quel sera l'état after
il s'exécute et (comme avec les monades régulières sur *
) quel type devalue
s le calcul produit.
Les morceaux habituels sont dans le *
sens comme une monade et dans le state
sens comme pour jouer aux dominos.
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 notion de "flèche de Kleisli" (fonction qui donne le calcul) ainsi générée est
a -> m i j b -- values a in, b out; state transition i to j
et on obtient une composition
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
et, comme toujours, les lois garantissent exactement cela ireturn
et icomp
nous donnent une catégorie
ireturn `icomp` g = g
f `icomp` ireturn = f
(f `icomp` g) `icomp` h = f `icomp` (g `icomp` h)
ou, dans la comédie faux C / Java / peu importe,
g(); skip = g()
skip; f() = f()
{g(); h()}; f() = h(); {g(); f()}
Pourquoi s'embêter? Modéliser des «règles» d'interaction. Par exemple, vous ne pouvez pas éjecter un DVD s'il n'y en a pas dans le lecteur, et vous ne pouvez pas mettre un DVD dans le lecteur s'il y en a déjà un. Alors
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
Avec ceci en place, nous pouvons définir les commandes "primitives"
dInsert :: DVD -> DVDDrive False True ()
dInsert dvd = DInsert dvd $ DReturn ()
dEject :: DVDrive True False DVD
dEject = DEject $ \ dvd -> DReturn dvd
à partir de laquelle d'autres sont assemblés avec ireturn
et ibind
. Maintenant, je peux écrire (emprunter do
-notation)
discSwap :: DVD -> DVDDrive True True DVD
discSwap dvd = do dvd' <- dEject; dInsert dvd ; ireturn dvd'
mais pas l'impossible physiquement
discSwap :: DVD -> DVDDrive True True DVD
discSwap dvd = do dInsert dvd; dEject -- ouch!
Alternativement, on peut définir directement ses commandes primitives
data DVDCommand :: Bool -> Bool -> * -> * where
InsertC :: DVD -> DVDCommand False True ()
EjectC :: DVDCommand True False DVD
puis instanciez le modèle générique
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 effet, nous avons dit ce que sont les flèches primitives de Kleisli (ce qu'est un "domino"), puis nous avons construit une notion appropriée de "séquence de calcul" sur elles.
Notez que pour chaque monade indexée m
, la "diagonale sans changement" m i i
est une monade, mais en général, m i j
ne l'est pas. De plus, les valeurs ne sont pas indexées mais les calculs sont indexés, donc une monade indexée n'est pas seulement l'idée habituelle de monade instanciée pour une autre catégorie.
Maintenant, regardez à nouveau le type de flèche Kleisli
a -> m i j b
Nous savons que nous devons être en état i
pour commencer, et nous prédisons que toute continuation commencera par l'état j
. Nous en savons beaucoup sur ce système! Ce n'est pas une opération risquée! Lorsque nous mettons le DVD dans le lecteur, il entre! Le lecteur DVD n'a aucun mot à dire sur l'état après chaque commande.
Mais ce n'est pas vrai en général, lorsqu'on interagit avec le monde. Parfois, vous devrez peut-être abandonner un peu de contrôle et laisser le monde faire ce qu'il veut. Par exemple, si vous êtes un serveur, vous pouvez proposer à votre client un choix et l'état de votre session dépendra de ce qu'il choisit. L'opération de "choix d'offre" du serveur ne détermine pas l'état résultant, mais le serveur devrait pouvoir continuer de toute façon. Ce n'est pas une "commande primitive" dans le sens ci-dessus, donc les monades indexées ne sont pas un si bon outil pour modéliser le scénario imprévisible .
Quel meilleur outil?
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
Des biscuits effrayants? Pas vraiment, pour deux raisons. Un, il semble un peu plus comme ce monade est, parce qu'il est une monade, mais plus (state -> *)
plutôt que *
. Deux, si vous regardez le type d'une flèche Kleisli,
a :-> m b = forall state. a state -> m b state
vous obtenez le type de calculs avec une précondition a
et une postcondition b
, tout comme dans Good Old Hoare Logic. Les affirmations dans la logique des programmes ont mis moins d'un demi-siècle à traverser la correspondance Curry-Howard et à devenir des types Haskell. Le type de returnIx
dit "vous pouvez réaliser n'importe quelle post-condition qui tient, juste en ne faisant rien", qui est la règle de Hoare Logic pour "sauter". La composition correspondante est la règle Hoare Logic pour ";".
Terminons en regardant le type de bindIx
, en mettant tous les quantificateurs.
bindIx :: forall i. m a i -> (forall j. a j -> m b j) -> m b i
Ces forall
s ont une polarité opposée. On choisit l'état initial i
, et un calcul qui peut commencer à i
, avec postcondition a
. Le monde choisit n'importe quel état intermédiaire j
qu'il aime, mais il doit nous donner la preuve que la post-condition b
tient, et à partir d'un tel état, nous pouvons continuer à b
tenir. Ainsi, en séquence, nous pouvons obtenir une condition à b
partir de l'état i
. En relâchant notre emprise sur les états «après», nous pouvons modéliser des calculs imprévisibles .
Les deux IxMonad
et MonadIx
sont utiles. Les deux modèles de validité des calculs interactifs par rapport à l'état changeant, prévisible et imprévisible, respectivement. La prévisibilité est précieuse lorsque vous pouvez l'obtenir, mais l'imprévisibilité est parfois une réalité. Espérons donc que cette réponse donne une indication de ce que sont les monades indexées, prédisant à la fois quand elles commencent à être utiles et quand elles s'arrêtent.
True
/False
comme arguments de type àDVDDrive
? Est-ce une extension, ou les booléens sont-ils réellement des types ici?