web-dev-qa-db-fra.com

Qu'est-ce que la monade indexée?

Qu'est-ce que monade indexée et la motivation de cette monade?

J'ai lu que cela aide à garder une trace des effets secondaires. Mais la signature et la documentation ne me mènent nulle part.

Quel serait un exemple de la façon dont il peut aider à garder une trace des effets secondaires (ou tout autre exemple valable)?

94
Sibi

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 des monades mais à proprement parler pas tout à fait. Le terme "monade indexée" fait partie 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 de ces notions, 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 d'un "calcul" (ou "action", si vous préférez, mais je m'en tiendrai au "calcul"), ressemble à

m before after value

before, after :: state et value :: *. L'idée est de capturer les moyens d'interagir en toute sécurité avec un système externe qui a une notion d'état prévisible. 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 de values le calcul produit.

Les morceaux habituels sont *- sage comme une monade et state- sage comme 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 nous obtenons 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 que 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/quoi que ce soit,

      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 insérer de 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 cela 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 instancier 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 n'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 d'une flèche de Kleisli

a -> m i j b

Nous savons que nous devons être dans l'état i pour commencer, et nous prévoyons que toute continuation commencera à partir de 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 de DVD n'a pas son mot à dire sur l'état après chaque commande.

Mais ce n'est pas vrai en général, lors de l'interaction avec le monde. Parfois, vous devrez peut-être abandonner un certain contrôle et laisser le monde faire ce qu'il veut. Par exemple, si vous êtes un serveur, vous pouvez offrir un choix à votre client et votre état de session dépendra de ce qu'il choisira. L'opération "choix de l'offre" du serveur ne détermine pas l'état résultant, mais le serveur doit quand même pouvoir continuer. Ce n'est pas une "commande primitive" dans le sens ci-dessus, donc les monades indexées ne sont pas un bon outil pour modéliser le scénario imprévisible.

Qu'est-ce qu'un 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. Premièrement, cela ressemble plutôt à ce qu'est une monade, car elle est une monade, mais sur (state -> *) plutôt que *. Deuxièmement, si vous regardez le type de flèche de Kleisli,

a :-> m b   =   forall state. a state -> m b state

vous obtenez le type de calculs avec préconditiona et postcondition b, comme dans Good Old Hoare Logic. Les assertions dans les logiques du programme ont mis moins d'un demi-siècle pour traverser la correspondance Curry-Howard et devenir des types Haskell. Le type de returnIx dit "vous pouvez réaliser n'importe quelle postcondition 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 dedans.

bindIx :: forall i. m a i -> (forall j. a j -> m b j) -> m b i

Ces foralls 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 postcondition b est valable, et à partir d'un tel état, nous pouvons continuer à faire b tenir. Ainsi, en séquence, nous pouvons atteindre la 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.

IxMonad et MonadIx sont utiles. Les deux modèles valident des calculs interactifs en ce qui concerne les changements d'état, prévisibles et imprévisibles, 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.

119
pigworker

Il existe au moins trois façons de définir une monade indexée que je connais.

Je ferai référence à ces options comme monades indexées à la X , où X s'étend sur les informaticiens Bob Atkey, Conor McBride et Dominic Orchard, comme cela c'est comme ça que j'ai tendance à penser à eux. Certaines parties de ces constructions ont une histoire beaucoup plus longue 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 cette réponse d'obtenir aussi ésotérique.

Atkey

Le style de Bob Atkey de la monade indexée consiste à 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:

class IMonad m where
  ireturn  ::  a -> m i i a
  ibind    ::  m i j a -> (a -> m j k b) -> m i k b

Nous pouvons également définir des comonades indexées à la Atkey également. En fait, j'en tire beaucoup de kilométrage dans la base de code lens .

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 monade indexée une forme plutôt intelligente.

Si nous définissons une transformation naturelle en utilisant la paramétricité comme suit

type a ~> b = forall i. a i -> b i 

alors nous pouvons écrire la définition de McBride comme

class IMonad m where
  ireturn :: a ~> m a
  ibind :: (a ~> m b) -> (m a ~> m b)

Cela semble assez différent de celui d'Atkey, mais cela ressemble plus à une monade normale, au lieu de construire une monade sur (m :: * -> *), nous le construisons sur (m :: (k -> *) -> (k -> *).

Fait intéressant, vous pouvez réellement récupérer le style de monade indexé d'Atkey à partir de McBride en utilisant un type de données intelligent, que McBride dans son style inimitable choisit de dire que vous devez lire comme "à la clé".

data (:=) :: a i j where
   V :: a -> (a := i) i

Maintenant, vous pouvez comprendre que

ireturn :: IMonad m => (a := j) ~> m (a := j)

qui s'étend à

ireturn :: IMonad m => (a := j) i -> m (a := j) i

ne peut être invoqué que lorsque j = i, puis une lecture attentive de ibind peut vous permettre de retrouver la même chose que Atkey's ibind. Vous devez contourner ces structures de données (: =), mais elles récupèrent la puissance de la présentation Atkey.

D'un autre côté, 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 dans une catégorie de foncteurs différente. Il fonctionne sur les endofuncteurs sur la catégorie des foncteurs de (k -> *) à (k -> *) plutôt que la catégorie de foncteurs de * à *.

Un exercice amusant consiste à découvrir comment effectuer la conversion McBride à Atkey pour les comonades indexées . J'utilise personnellement un type de données "At" pour la construction "à clé" dans l'article de McBride. En fait, je suis allé voir Bob Atkey à l'ICFP 2013 et j'ai mentionné que je l'avais transformé à l'envers pour en faire un "manteau". Il semblait visiblement perturbé. La ligne s'est mieux déroulée dans ma tête. =)

Verger

Enfin, un troisième demandeur bien moins souvent référencé au nom de "monade indexée" est dû à Dominic Orchard, où il utilise à la place un monoïde de niveau type pour briser les indices. Plutôt que de passer par les détails de la construction, je vais simplement faire un lien vers cet exposé:

http://www.cl.cam.ac.uk/~dao29/ixmonad/ixmonad-fita14.pdf

45
Edward KMETT

Comme scénario simple, supposez que vous avez une monade d'état. Le type d'état est un grand complexe, mais tous ces états peuvent être partitionnés en deux ensembles: les états rouge et bleu. Certaines opérations de 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 rendront l'état rouge (blueToRed). Dans une monade ordinaire, nous pourrions écrire

blueToRed  :: State S ()
blueToBlue :: State S ()

foo :: State S ()
foo = do blueToRed
         blueToBlue

déclencher une erreur d'exécution car la deuxième action attend un état bleu. Nous aimerions empêcher cela statiquement. La monade indexée remplit cet objectif:

data Red
data Blue

-- assume a new indexed State monad
blueToRed  :: State S Blue Red  ()
blueToBlue :: State S Blue Blue ()

foo :: State S ?? ?? ()
foo = blueToRed `ibind` \_ ->
      blueToBlue          -- type error

Une erreur de type est déclenchée car le deuxième index de blueToRed (Red) diffère du premier index de blueToBlue (Blue).

Comme autre exemple, avec les monades indexées, vous pouvez autoriser une monade d'état à changer le type de son état, par ex. tu aurais pu

data State old new a = State (old -> (new, a))

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 du type

Push :: a -> State old (a,old) ()
pop  :: State (a,new) new a

Comme autre exemple, supposons que vous vouliez une monade restreinte IO qui n'autorise pas l'accès aux fichiers. Vous pouvez utiliser par exemple.

openFile :: IO any FilesAccessed ()
newIORef :: a -> IO any any (IORef a)
-- no operation of type :: IO any NoAccess _

De cette façon, une action de type IO ... NoAccess () est statiquement garantie sans accès aux fichiers. Au lieu de cela, une action de type IO ... FilesAccessed () peut accéder aux fichiers. Avoir une monade indexée signifierait que vous n'avez pas à créer un type distinct pour l'IO restreint, ce qui nécessiterait de dupliquer toutes les fonctions non liées aux fichiers dans les deux types IO.

23
chi

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 serait IndexedMonad m => m i j ai et j sont des types d'index de sorte que i est le type de l'index au début du calcul monadique et j à la fin du calcul. D'une certaine manière, vous pouvez considérer i comme une sorte de type d'entrée et j comme type de sortie.

En utilisant State comme exemple, un calcul avec état State s a conserve un état de type s tout au long du calcul et renvoie un résultat de type a. Une version indexée, IndexedState i j a, est un calcul avec état où l'état peut changer en un type différent pendant le calcul. L'état initial a le type i et l'état et la fin du calcul a le type j.

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.

17
shang

Il peut être important de voir 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 est valable.

Par exemple (dans agda), vous pouvez spécifier que certains nombres naturels sont liés à _<_, et le type indique de quels nombres il s'agit. Ensuite, vous pouvez exiger qu'une fonction reçoive un témoin qui m < n, car alors seulement la fonction fonctionne correctement - et sans fournir un tel témoin, le programme ne compilera pas.

Comme autre exemple, étant donné suffisamment de persévérance et de prise en charge du compilateur pour la langue que vous avez choisie, vous pouvez encoder 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.

5
Sassa NF