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 :: state
e 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 before
lo stato che esegue, quale sarà lo stato after
e (come con le monadi regolari *
) di che tipovalue
s il calcolo produce.
I soliti pezzi e pezzi sono *
come una monade e state
come 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 ireturn
e icomp
ci 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 ireturn
e ibind
. Ora posso scrivere ( do
annotazione 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 j
non 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 i
per 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 returnIx
dice "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 forall
hanno polarità opposta. Scegliamo lo stato iniziale i
e un calcolo che può iniziare da i
, con postcondizione a
. Il mondo sceglie qualsiasi stato intermedio j
che gli piace, ma deve darci la prova che la postcondizione b
vale, e da qualsiasi stato del genere possiamo continuare a far valere b
. Quindi, in sequenza, possiamo ottenere la condizione b
dallo stato i
. Rilasciando la nostra presa sugli stati "dopo", possiamo modellare calcoli imprevedibili .
Entrambi IxMonad
e MonadIx
sono 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.
True
/False
come argomenti di tipo aDVDDrive
? È un'estensione o i booleani sono effettivamente digitati qui?