Como sempre, a terminologia que as pessoas usam não é totalmente consistente. Há uma variedade de noções inspiradas em mônadas, mas, estritamente falando, não são exatamente. O termo "mônada indexada" é um de vários (incluindo "monadish" e "mônada parametrizada" (o nome de Atkey para eles)) de termos usados para caracterizar tal noção. (Outra noção, se você estiver interessado, é a "mônada de efeito paramétrico" de Katsumata, indexada por um monóide, onde o retorno é indexado de forma neutra e o vínculo se acumula em seu índice.)
Em primeiro lugar, vamos verificar os tipos.
IxMonad (m :: state -> state -> * -> *)
Ou seja, o tipo de "cálculo" (ou "ação", se preferir, mas ficarei com "cálculo"), parece
m before after value
onde before, after :: state
e value :: *
. A ideia é capturar os meios para interagir com segurança com um sistema externo que tenha alguma noção previsível de estado. O tipo de um cálculo diz a você qual deve ser o estado em que before
ele executa, qual será o estado em que after
ele executa e (como com mônadas regulares *
) que tipo de value
s o cálculo produz.
As partes e peças usuais são - *
sábias como uma mônada e - state
como jogar dominó.
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
A noção de "seta de Kleisli" (função que produz computação) assim gerada é
a -> m i j b -- values a in, b out; state transition i to j
e obtemos uma composição
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, como sempre, as leis garantem exatamente isso ireturn
e icomp
nos dão uma categoria
ireturn `icomp` g = g
f `icomp` ireturn = f
(f `icomp` g) `icomp` h = f `icomp` (g `icomp` h)
ou, na comédia falsa C / Java / qualquer coisa,
g(); skip = g()
skip; f() = f()
{g(); h()}; f() = h(); {g(); f()}
Porque se importar? Modelar "regras" de interação. Por exemplo, você não pode ejetar um DVD se não houver um na unidade, e você não pode colocar um DVD na unidade se já houver um nele. assim
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
Com isso no lugar, podemos definir os comandos "primitivos"
dInsert :: DVD -> DVDDrive False True ()
dInsert dvd = DInsert dvd $ DReturn ()
dEject :: DVDrive True False DVD
dEject = DEject $ \ dvd -> DReturn dvd
do qual outros são montados com ireturn
e ibind
. Agora, posso escrever (emprestando- do
anotação)
discSwap :: DVD -> DVDDrive True True DVD
discSwap dvd = do dvd' <- dEject; dInsert dvd ; ireturn dvd'
mas não o fisicamente impossível
discSwap :: DVD -> DVDDrive True True DVD
discSwap dvd = do dInsert dvd; dEject -- ouch!
Alternativamente, pode-se definir seus comandos primitivos diretamente
data DVDCommand :: Bool -> Bool -> * -> * where
InsertC :: DVD -> DVDCommand False True ()
EjectC :: DVDCommand True False DVD
e instanciar o modelo genérico
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
Na verdade, dissemos o que são as flechas de Kleisli primitivas (o que um "dominó" é) e, em seguida, construímos uma noção adequada de "sequência de computação" sobre elas.
Observe que para cada mônada indexada m
, a "diagonal sem mudança" m i i
é uma mônada, mas, em geral, m i j
não é. Além disso, os valores não são indexados, mas os cálculos são indexados; portanto, uma mônada indexada não é apenas a ideia usual de mônada instanciada para alguma outra categoria.
Agora, olhe novamente para o tipo de flecha de Kleisli
a -> m i j b
Sabemos que devemos estar no estado i
para começar e prevemos que qualquer continuação começará do estado j
. Sabemos muito sobre este sistema! Esta não é uma operação arriscada! Quando colocamos o DVD no drive, ele entra! O drive de DVD não diz qual é o estado após cada comando.
Mas isso não é verdade em geral, ao interagir com o mundo. Às vezes, você pode precisar abrir mão de algum controle e deixar o mundo fazer o que quiser. Por exemplo, se você for um servidor, poderá oferecer uma escolha ao seu cliente e o estado da sua sessão dependerá do que ele escolher. A operação de "escolha de oferta" do servidor não determina o estado resultante, mas o servidor deve ser capaz de continuar de qualquer maneira. Não é um "comando primitivo" no sentido acima, então mônadas indexadas não são uma ferramenta tão boa para modelar o cenário imprevisível .
Qual é a melhor ferramenta?
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
Biscoitos assustadores? Na verdade não, por dois motivos. Um, parece um pouco mais com o que é uma mônada, porque é uma mônada, mas acabou (state -> *)
ao invés de *
. Dois, se você olhar para o tipo de flecha de Kleisli,
a :-> m b = forall state. a state -> m b state
você obtém o tipo de cálculos com uma pré - condiçãoa
e uma pós-condição b
, assim como em Good Old Hoare Logic. As afirmações em lógicas de programa levaram menos de meio século para cruzar a correspondência Curry-Howard e se tornarem tipos Haskell. O tipo de returnIx
diz "você pode alcançar qualquer pós-condição que se mantenha, simplesmente sem fazer nada", que é a regra da lógica de Hoare para "pular". A composição correspondente é a regra da lógica de Hoare para ";".
Vamos terminar examinando o tipo de bindIx
, colocando todos os quantificadores em.
bindIx :: forall i. m a i -> (forall j. a j -> m b j) -> m b i
Esses forall
s têm polaridade oposta. Escolhemos o estado inicial i
e um cálculo que pode começar i
com a pós-condição a
. O mundo escolhe qualquer estado intermediário de j
sua preferência, mas deve nos dar a evidência de que a pós-condição se b
mantém e, de qualquer estado desse tipo, podemos prosseguir para b
alcançá-la. Então, em sequência, podemos alcançar a condição b
do estado i
. Ao liberar nosso controle sobre os estados "depois", podemos modelar cálculos imprevisíveis .
Ambos IxMonad
e MonadIx
são úteis. Ambos modelam a validade de cálculos interativos com relação à mudança de estado, previsível e imprevisível, respectivamente. A previsibilidade é valiosa quando você pode obtê-la, mas às vezes a imprevisibilidade é um fato da vida. Esperançosamente, então, esta resposta dá alguma indicação do que são as mônadas indexadas, prevendo quando elas começam a ser úteis e quando elas param.
True
/False
como argumentos de tipo paraDVDDrive
? Isso é alguma extensão ou os booleanos são realmente tipos aqui?