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?DataKinds
extension et dans des langages typés de manière dépendante ... eh bien, c'est un peu le tout.MonadIx
, peut-être avec des exemples? Est-ce mieux pour des raisons théoriques ou mieux pour une application pratique?RebindableSyntax
extension. Une mention d'autres extensions requises serait bien, comme celle mentionnée ciDataKinds
Il y a au moins trois façons de définir une monade indexée que je connais.
J'appellerai ces options des monades indexées à la X , où X s'étend sur les informaticiens Bob Atkey, Conor McBride et Dominic Orchard, car c'est ainsi que j'ai tendance à les considérer. Certaines parties de ces constructions ont une histoire beaucoup plus longue et plus illustre et des interprétations plus agréables grâce à la théorie des catégories, mais j'ai d'abord appris qu'elles étaient associées à ces noms, et j'essaie d'empêcher que cette réponse ne devienne trop ésotérique.
Atkey
Le style de monade indexée de Bob Atkey est de travailler avec 2 paramètres supplémentaires pour gérer l'index de la monade.
Avec cela, vous obtenez les définitions que les gens ont jetées dans d'autres réponses:
Nous pouvons également définir des comonades indexées à la Atkey. En fait, je tire beaucoup de kilomètres de ceux de la
lens
base de code .McBride
La prochaine forme de monade indexée est la définition de Conor McBride tirée de son article "Kleisli Arrows of Outrageous Fortune" . Il utilise à la place un seul paramètre pour l'index. Cela donne à la définition de la monade indexée une forme plutôt intelligente.
Si nous définissons une transformation naturelle en utilisant la paramétrie comme suit
alors nous pouvons écrire la définition de McBride comme
Cela semble assez différent de celui d'Atkey, mais cela ressemble plus à une Monade normale, au lieu de construire une monade
(m :: * -> *)
, nous la construisons(m :: (k -> *) -> (k -> *)
.Il est intéressant de noter que vous pouvez en fait récupérer le style de monade indexée d'Atkey à partir de McBride en utilisant un type de données intelligent, que McBride dans son style inimitable choisit de dire que vous devriez lire comme "at key".
Maintenant tu peux comprendre ça
qui s'étend à
ne peut être invoqué que lorsque j = i, puis une lecture attentive de
ibind
peut vous ramener à la même chose que celle d'Atkeyibind
. Vous devez contourner ces structures de données (: =), mais elles récupèrent la puissance de la présentation Atkey.En revanche, la présentation Atkey n'est pas assez forte pour récupérer toutes les utilisations de la version de McBride. Le pouvoir a été strictement acquis.
Une autre bonne chose est que la monade indexée de McBride est clairement une monade, c'est juste une monade sur une catégorie de foncteurs différente. Il fonctionne sur les endofoncteurs sur la catégorie des foncteurs de
(k -> *)
à(k -> *)
plutôt que sur la catégorie des foncteurs de*
à*
.Un exercice amusant consiste à trouver comment faire la conversion McBride en Atkey pour les comonads indexés . J'utilise personnellement un type de données «At» pour la construction «at key» dans l'article de McBride. En fait, je me suis approché de Bob Atkey à l'ICFP 2013 et j'ai mentionné que je l'avais transformé à l'envers pour en faire un "manteau". Il semblait visiblement dérangé. La réplique s'est mieux déroulée dans ma tête. =)
Verger
Enfin, Dominic Orchard, où il utilise à la place un monoïde de niveau de type pour écraser les indices, est un troisième demandeur beaucoup moins souvent référencé sous le nom de "monade indexée". Plutôt que de passer en revue les détails de la construction, je vais simplement faire un lien vers cette conférence:
https://github.com/dorchard/effect-monad/blob/master/docs/ixmonad-fita14.pdf
la source
ibind
": Introduisez l'alias de typeAtkey m i j a = m (a := j) i
. L'utilisation de ceci commem
définition d'Atkey récupère les deux signatures que nous recherchons:ireturnAtkin :: a -> m (a := i) i
etibindAtkin :: m (a := j) i -> (a -> m (b := k) j) -> m (b := k) i
. Le premier est obtenu par composition:ireturn . V
. Le second en (1) construisant une fonctionforall j. (a := j) j -> m (b := k) j
par correspondance de modèle, puis en passant le récupéréa
au deuxième argument deibindAtkin
.En tant que scénario simple, supposons que vous ayez une monade d'État. Le type d'état est complexe, mais tous ces états peuvent être partitionnés en deux ensembles: les états rouge et bleu. Certaines opérations dans cette monade n'ont de sens que si l'état actuel est un état bleu. Parmi ceux-ci, certains garderont l'état bleu (
blueToBlue
), tandis que d'autres le rendront rouge (blueToRed
). Dans une monade ordinaire, nous pourrions écriredéclenchant une erreur d'exécution car la deuxième action attend un état bleu. Nous aimerions éviter cela de manière statique. La monade indexée remplit cet objectif:
Une erreur de type est déclenchée car le deuxième index de
blueToRed
(Red
) diffère du premier index deblueToBlue
(Blue
).Comme autre exemple, avec les monades indexées, vous pouvez autoriser une monade d'état à changer le type de son état, par exemple vous pourriez avoir
Vous pouvez utiliser ce qui précède pour créer un état qui est une pile hétérogène de type statique. Les opérations auraient le type
Comme autre exemple, supposons que vous vouliez une monade IO restreinte qui n'autorise pas l'accès aux fichiers. Vous pouvez utiliser par exemple
De cette manière, une action de type
IO ... NoAccess ()
est statiquement garantie sans accès aux fichiers. Au lieu de cela, une action de typeIO ... FilesAccessed ()
peut accéder aux fichiers. Avoir une monade indexée signifierait que vous n'avez pas à créer un type distinct pour l'E / S restreinte, ce qui nécessiterait de dupliquer chaque fonction non liée à un fichier dans les deux types d'E / S.la source
Une monade indexée n'est pas une monade spécifique comme, par exemple, la monade d'état, mais une sorte de généralisation du concept de monade avec des paramètres de type supplémentaires.
Alors qu'une valeur monadique "standard" a le type,
Monad m => m a
une valeur dans une monade indexée seraitIndexedMonad m => m i j a
oùi
etj
sont des types d'index de sorte quei
c'est le type de l'index au début du calcul monadique etj
à la fin du calcul. D'une certaine manière, vous pouvez penseri
à une sorte de type d'entrée etj
de type de sortie.En utilisant
State
comme exemple, un calcul avec étatState s a
maintient un état de types
tout au long du calcul et renvoie un résultat de typea
. Une version indexéeIndexedState i j a
, est un calcul avec état dans lequel l'état peut passer à un type différent pendant le calcul. L'état initial a le typei
et l'état et la fin du calcul a le typej
.L'utilisation d'une monade indexée sur une monade normale est rarement nécessaire mais elle peut être utilisée dans certains cas pour coder des garanties statiques plus strictes.
la source
Il peut être important de regarder comment l'indexation est utilisée dans les types dépendants (par exemple dans agda). Cela peut expliquer comment l'indexation aide en général, puis traduire cette expérience en monades.
L'indexation permet d'établir des relations entre des instances particulières de types. Ensuite, vous pouvez raisonner sur certaines valeurs pour déterminer si cette relation tient.
Par exemple (dans agda), vous pouvez spécifier que certains nombres naturels sont liés
_<_
, et le type indique de quels nombres ils sont. Ensuite, vous pouvez exiger qu'une fonction soit donnée à un témoin quim < n
, parce que ce n'est qu'alors que la fonction fonctionne correctement - et sans fournir un tel témoin, le programme ne se compilera pas.Comme autre exemple, étant donné suffisamment de persévérance et de support du compilateur pour le langage choisi, vous pouvez coder que la fonction suppose qu'une certaine liste est triée.
Les monades indexées permettent d'encoder une partie de ce que font les systèmes de types dépendants, pour gérer plus précisément les effets secondaires.
la source