Cos'è la monade indicizzata?


98

Qual è la monade indicizzata e la motivazione per questa monade?

Ho letto che aiuta a tenere traccia degli effetti collaterali. Ma la firma del tipo e la documentazione non mi portano da nessuna parte.

Quale potrebbe essere un esempio di come può aiutare a tenere traccia degli effetti collaterali (o qualsiasi altro esempio valido)?

Risposte:


123

Come sempre, la terminologia utilizzata dalle persone non è del tutto coerente. C'è una varietà di nozioni ispirate alle monadi ma, in senso stretto, non sono del tutto. Il termine "monade indicizzata" è uno di un numero (inclusi "monade" e "monade parametrizzata" (il nome di Atkey per loro)) di termini usati per caratterizzare una di queste nozioni. (Un'altra nozione simile, se sei interessato, è la "monade dell'effetto parametrico" di Katsumata, indicizzata da un monoide, dove il rendimento è indicizzato in modo neutro e il legame si accumula nel suo indice.)

Prima di tutto, controlliamo i tipi.

IxMonad (m :: state -> state -> * -> *)

Cioè, il tipo di "computazione" (o "azione", se preferisci, ma mi limiterò a "computazione"), sembra

m before after value

dove before, after :: statee value :: *. L'idea è di catturare i mezzi per interagire in sicurezza con un sistema esterno che ha una nozione prevedibile di stato. Il tipo di calcolo ti dice quale deve essere beforelo stato che esegue, quale sarà lo stato aftere (come con le monadi regolari *) di che tipovalue s il calcolo produce.

I soliti pezzi e pezzi sono *come una monade e statecome giocare a domino.

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 nozione di "freccia di Kleisli" (funzione che fornisce il calcolo) così generata è

a -> m i j b   -- values a in, b out; state transition i to j

e otteniamo una composizione

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

e, come sempre, le leggi lo assicurano esattamente ireturne icompci danno una categoria

      ireturn `icomp` g = g
      f `icomp` ireturn = f
(f `icomp` g) `icomp` h = f `icomp` (g `icomp` h)

oppure, nella commedia fake C / Java / qualunque cosa,

      g(); skip = g()
      skip; f() = f()
{g(); h()}; f() = h(); {g(); f()}

Perché preoccuparsi? Per modellare "regole" di interazione. Ad esempio, non puoi espellere un dvd se non ce n'è uno nell'unità e non puoi mettere un dvd nell'unità se ce n'è già uno dentro. Così

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 questo a posto, possiamo definire i comandi "primitivi"

dInsert :: DVD -> DVDDrive False True ()
dInsert dvd = DInsert dvd $ DReturn ()

dEject :: DVDrive True False DVD
dEject = DEject $ \ dvd -> DReturn dvd

da cui altri sono assemblati con ireturne ibind. Ora posso scrivere ( doannotazione in prestito )

discSwap :: DVD -> DVDDrive True True DVD
discSwap dvd = do dvd' <- dEject; dInsert dvd ; ireturn dvd'

ma non l'impossibile fisicamente

discSwap :: DVD -> DVDDrive True True DVD
discSwap dvd = do dInsert dvd; dEject      -- ouch!

In alternativa, si possono definire direttamente i propri comandi primitivi

data DVDCommand :: Bool -> Bool -> * -> * where
  InsertC  :: DVD -> DVDCommand False True ()
  EjectC   :: DVDCommand True False DVD

e quindi creare un'istanza del modello generico

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

In effetti, abbiamo detto cosa sono le primitive frecce di Kleisli (cos'è un "domino"), quindi abbiamo costruito su di esse una nozione adeguata di "sequenza di calcolo".

Si noti che per ogni monade indicizzata m, la "diagonale senza cambiamento" m i iè una monade, ma in generale m i jnon lo è. Inoltre, i valori non sono indicizzati ma i calcoli sono indicizzati, quindi una monade indicizzata non è solo la solita idea di monade istanziata per qualche altra categoria.

Ora guarda di nuovo il tipo di freccia di Kleisli

a -> m i j b

Sappiamo che dobbiamo essere nello stato iper iniziare e prevediamo che qualsiasi continuazione inizierà dallo stato j. Sappiamo molto di questo sistema! Questa non è un'operazione rischiosa! Quando inseriamo il dvd nell'unità, entra! L'unità dvd non ha voce in capitolo su quale sia lo stato dopo ogni comando.

Ma questo non è vero in generale, quando si interagisce con il mondo. A volte potresti dover dare via un po 'di controllo e lasciare che il mondo faccia ciò che vuole. Ad esempio, se sei un server, potresti offrire al tuo client una scelta e lo stato della tua sessione dipenderà da ciò che scelgono. L'operazione di "scelta dell'offerta" del server non determina lo stato risultante, ma il server dovrebbe essere in grado di continuare comunque. Non è un "comando primitivo" nel senso precedente, quindi le monadi indicizzate non sono un ottimo strumento per modellare lo scenario imprevedibile .

Qual è uno strumento migliore?

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

Biscotti spaventosi? Non proprio, per due ragioni. Uno, sembra un po 'più simile a ciò che una monade è, perché è una monade, ma nel corso (state -> *)piuttosto che *. Due, se guardi il tipo di freccia di Kleisli,

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

si ottiene il tipo di calcolo con una precondizione a e una postcondizione b, proprio come in Good Old Hoare Logic. Le affermazioni nelle logiche di programma hanno impiegato meno di mezzo secolo per attraversare la corrispondenza Curry-Howard e diventare tipi Haskell. Il tipo returnIxdice "puoi ottenere qualsiasi postcondizione valida, semplicemente non facendo nulla", che è la regola di Hoare Logic per "saltare". La composizione corrispondente è la regola Hoare Logic per ";".

Finiamo esaminando il tipo di bindIx, inserendo tutti i quantificatori.

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

Questi forallhanno polarità opposta. Scegliamo lo stato iniziale ie un calcolo che può iniziare da i, con postcondizione a. Il mondo sceglie qualsiasi stato intermedio jche gli piace, ma deve darci la prova che la postcondizione bvale, e da qualsiasi stato del genere possiamo continuare a far valere b. Quindi, in sequenza, possiamo ottenere la condizione bdallo stato i. Rilasciando la nostra presa sugli stati "dopo", possiamo modellare calcoli imprevedibili .

Entrambi IxMonade MonadIxsono utili. Entrambi modellano la validità dei calcoli interattivi rispetto al cambiamento di stato, rispettivamente prevedibile e imprevedibile. La prevedibilità è preziosa quando puoi ottenerla, ma l'imprevedibilità a volte è un dato di fatto. Si spera, quindi, che questa risposta dia qualche indicazione su cosa sono le monadi indicizzate, prevedendo sia quando iniziano ad essere utili sia quando si fermano.


1
Come puoi passare i valori True/ Falsecome argomenti di tipo a DVDDrive? È un'estensione o i booleani sono effettivamente digitati qui?
Bergi

8
@Bergi I booleani sono stati "sollevati" per esistere a livello di tipo. Questo è possibile in Haskell usando l' DataKindsestensione e in linguaggi tipizzati in modo dipendente ... beh, questo è più o meno il tutto.
J. Abrahamson

Potresti approfondire un po ' MonadIx, magari con degli esempi? È meglio su basi teoriche o meglio per l'applicazione pratica?
Christian Conkle

2
@ChristianConkle mi rendo conto che non è molto utile. Ma sollevi quella che è davvero tutta un'altra domanda. A livello locale, quando dico che MonadIx è "migliore", intendo nel contesto della modellazione delle interazioni con un ambiente imprevedibile. Ad esempio, se il tuo lettore DVD è autorizzato a sputare DVD, non gli piace quando provi a inserirli. Alcune situazioni pratiche si comportano così male. Altri hanno una maggiore prevedibilità (nel senso che puoi dire in quale stato inizia qualsiasi continuazione, non che le operazioni non falliscono), nel qual caso IxMonad è più facile da lavorare.
operaio suino

1
Quando si "prende in prestito" la notazione do nella risposta, potrebbe essere utile dire che è effettivamente una sintassi valida con l' RebindableSyntaxestensione. Sarebbe bello menzionare altre estensioni richieste, come il già citatoDataKinds
gigabyte

46

Ci sono almeno tre modi per definire una monade indicizzata che conosco.

Mi riferirò a queste opzioni come monadi indicizzate à la X , dove X spazia dagli scienziati informatici Bob Atkey, Conor McBride e Dominic Orchard, poiché è così che tendo a pensarle. Parti di queste costruzioni hanno una storia molto più illustre e interpretazioni più piacevoli attraverso la teoria delle categorie, ma prima ho appreso di esse associate a questi nomi e sto cercando di evitare che questa risposta diventi troppo esoterica.

Atkey

Lo stile della monade indicizzata di Bob Atkey consiste nel lavorare con 2 parametri extra per gestire l'indice della monade.

Con questo ottieni le definizioni che le persone hanno gettato in altre risposte:

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

Possiamo anche definire comonad indicizzate à la Atkey. In realtà ottengo un sacco di chilometri da quelli nel lenscodice base .

McBride

La forma successiva di monade indicizzata è la definizione di Conor McBride dal suo articolo "Kleisli Arrows of Outrageous Fortune" . Utilizza invece un singolo parametro per l'indice. Questo fa sì che la definizione della monade indicizzata abbia una forma piuttosto intelligente.

Se definiamo una trasformazione naturale utilizzando la parametricità come segue

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

quindi possiamo scrivere la definizione di McBride come

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

Questo sembra molto diverso da quello di Atkey, ma sembra più una normale Monade, invece di costruire una monade su di essa (m :: * -> *), la costruiamo su (m :: (k -> *) -> (k -> *).

È interessante notare che puoi effettivamente recuperare lo stile di Atkey della monade indicizzata da McBride usando un tipo di dati intelligente, che McBride nel suo stile inimitabile sceglie di dire che dovresti leggere come "alla chiave".

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

Ora puoi risolverlo

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

che si espande in

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

può essere invocato solo quando j = i, e quindi un'attenta lettura di ibindpuò riportarti allo stesso modo di Atkey ibind. È necessario passare queste strutture di dati (: =), ma recuperano la potenza della presentazione Atkey.

D'altra parte, la presentazione Atkey non è abbastanza forte da recuperare tutti gli usi della versione di McBride. Il potere è stato rigorosamente guadagnato.

Un'altra cosa bella è che la monade indicizzata di McBride è chiaramente una monade, è solo una monade su una diversa categoria di funtori. Funziona su endofunctors sulla categoria di funtori da (k -> *)a (k -> *)piuttosto che sulla categoria di funtori da *a *.

Un esercizio divertente è capire come eseguire la conversione da McBride ad Atkey per comonad indicizzate . Personalmente uso un tipo di dati "At" per la costruzione "at key" nel documento di McBride. In realtà mi sono avvicinato a Bob Atkey all'ICFP 2013 e ho detto che l'avevo rovesciato e l'ho trasformato in un "cappotto". Sembrava visibilmente turbato. La linea suonava meglio nella mia testa. =)

Frutteto

Infine, un terzo richiedente molto meno comunemente riferito al nome di "monade indicizzata" è dovuto a Dominic Orchard, dove utilizza invece un monoide a livello di tipo per frantumare gli indici. Piuttosto che passare attraverso i dettagli della costruzione, mi collegherò semplicemente a questo discorso:

https://github.com/dorchard/effect-monad/blob/master/docs/ixmonad-fita14.pdf


1
Ho ragione che la monade di Orchard è equivalente a quella di Atkey, in quanto possiamo passare dalla prima alla seconda prendendo l'endomorfismo monoide, e tornare indietro codificando CPS con aggiunte monoidali nella transizione di stato?
András Kovács

Mi sembra plausibile.
Edward KMETT

Detto questo, sulla base di qualcosa che mi ha detto all'ICFP 2013, credo che Orchard intendesse che le sue famiglie tipo si comportassero come un vero monoide piuttosto che una categoria arbitraria in cui alcune delle frecce non possono connettersi, quindi potrebbe esserci di più nella storia oltre a ciò, poiché la costruzione di Atkey ti consente di limitare facilmente alcune azioni di Kleisli dal connettersi con altre - in molti modi questo è il vero punto di essa e della versione di McBride.
Edward KMETT

2
Per espandere la "lettura attenta di ibind": introdurre l'alias di tipo Atkey m i j a = m (a := j) i. Usando questo come mnella definizione di Atkey si recuperano le due firme che cerchiamo: ireturnAtkin :: a -> m (a := i) ie ibindAtkin :: m (a := j) i -> (a -> m (b := k) j) -> m (b := k) i. Il primo è ottenuto composizione: ireturn . V. Il secondo da (1) costruendo una funzione forall j. (a := j) j -> m (b := k) jtramite il pattern matching, quindi passando il recuperato aal secondo argomento di ibindAtkin.
WorldSEnder

23

Come semplice scenario, supponi di avere una monade di stato. Il tipo di stato è complesso e ampio, tuttavia tutti questi stati possono essere suddivisi in due gruppi: stati rosso e blu. Alcune operazioni in questa monade hanno senso solo se lo stato corrente è uno stato blu. Tra questi, alcuni manterranno lo stato blu (blueToBlue ), mentre altri renderanno rosso ( blueToRed). In una monade normale, potremmo scrivere

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

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

innescando un errore di runtime poiché la seconda azione prevede uno stato blu. Vorremmo impedirlo staticamente. La monade indicizzata soddisfa questo obiettivo:

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

Viene generato un errore di tipo perché il secondo indice di blueToRed( Red) è diverso dal primo indice di blueToBlue( Blue).

Come altro esempio, con le monadi indicizzate puoi consentire a una monade di stato di cambiare il tipo per il suo stato, ad esempio potresti avere

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

È possibile utilizzare quanto sopra per creare uno stato che è uno stack eterogeneo tipizzato staticamente. Le operazioni avrebbero tipo

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

Come altro esempio, supponiamo di volere una monade IO limitata che non consente l'accesso ai file. Potresti usare ad es

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

In questo modo, un'azione di tipo IO ... NoAccess ()è staticamente garantita come priva di accesso ai file. Invece, un'azione di tipo IO ... FilesAccessed ()può accedere ai file. Avere una monade indicizzata significherebbe che non è necessario creare un tipo separato per l'IO con restrizioni, che richiederebbe la duplicazione di ogni funzione non correlata al file in entrambi i tipi di IO.


18

Una monade indicizzata non è una monade specifica come, ad esempio, la monade di stato, ma una sorta di generalizzazione del concetto di monade con parametri di tipo extra.

Mentre un valore monadico "standard" ha il tipo, Monad m => m aun valore in una monade indicizzata sarebbe IndexedMonad m => m i j adove ie jsono tipi di indice, quindi questo iè il tipo di indice all'inizio del calcolo monadico e jalla fine del calcolo. In un certo senso, puoi pensare ia una sorta di tipo di input ej al tipo di output.

Usando Statecome esempio, un calcolo con stato State s amantiene uno stato di tipo sdurante il calcolo e restituisce un risultato di tipo a. Una versione indicizzata IndexedState i j a,, è un calcolo con stato in cui lo stato può cambiare in un tipo diverso durante il calcolo. Lo stato iniziale ha il tipo ie lo stato e la fine del calcolo ha il tipo j.

L'uso di una monade indicizzata su una monade normale è raramente necessario, ma in alcuni casi può essere utilizzato per codificare garanzie statiche più rigide.


5

Potrebbe essere importante dare un'occhiata a come l'indicizzazione viene utilizzata nei tipi dipendenti (ad esempio in agda). Questo può spiegare come l'indicizzazione aiuta in generale, quindi tradurre questa esperienza in monadi.

L'indicizzazione consente di stabilire relazioni tra particolari istanze di tipi. Quindi puoi ragionare su alcuni valori per stabilire se quella relazione è valida.

Ad esempio (in agda) puoi specificare che alcuni numeri naturali sono correlati con _<_, e il tipo dice quali numeri sono. Quindi è possibile richiedere che a qualche funzione venga dato un testimone che m < n, perché solo allora la funzione funziona correttamente - e senza fornire tale testimone il programma non verrà compilato.

Come altro esempio, dato abbastanza perseveranza e supporto del compilatore per la lingua scelta, potresti codificare che la funzione presume che un certo elenco sia ordinato.

Le monadi indicizzate consentono di codificare alcune delle attività dei sistemi di tipo dipendente, per gestire gli effetti collaterali in modo più preciso.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.