Enquanto aprendia Haskell, enfrentei muitos tutoriais tentando explicar o que são mônadas e por que as mônadas são importantes em Haskell. Cada um deles usou analogias para que fosse mais fácil entender o significado. No final do dia, acabei com três visões diferentes do que é uma mônada:
Vista 1: Mônada como um rótulo
Às vezes acho que uma mônada como um rótulo para um tipo específico. Por exemplo, uma função do tipo:
myfunction :: IO Int
myfunction é uma função que sempre que é executada gera um valor Int. O tipo do resultado não é Int, mas IO Int. Portanto, IO é um rótulo do valor Int alertando o usuário para saber que o valor Int é o resultado de um processo em que uma ação de IO foi feita.
Conseqüentemente, esse valor Int foi marcado como um valor proveniente de um processo com IO, portanto, esse valor é "sujo". Seu processo não é mais puro.
Vista 2: Mônada como um espaço privado onde coisas desagradáveis podem acontecer.
Em um sistema em que todo o processo é puro e rigoroso, às vezes você precisa ter efeitos colaterais. Assim, uma mônada é apenas um pequeno espaço que lhe permite fazer efeitos colaterais desagradáveis. Nesse espaço, você pode escapar do mundo puro, ficar impuro, fazer seu processo e depois voltar com um valor.
Vista 3: Mônada como na teoria das categorias
Esta é a visão que eu não entendo completamente. Uma mônada é apenas um functor para a mesma categoria ou subcategoria. Por exemplo, você tem os valores Int e como uma subcategoria IO Int, que são os valores Int gerados após um processo de IO.
Essas visualizações estão corretas? Qual é mais preciso?
Respostas:
As visualizações 1 e 2 estão incorretas em geral.
* -> *
pode funcionar como um rótulo; as mônadas são muito mais que isso.IO
mônada) os cálculos dentro de uma mônada não são impuros. Eles simplesmente representam cálculos que percebemos como tendo efeitos colaterais, mas são puros.Esses dois mal-entendidos vêm do foco na
IO
mônada, que é realmente um pouco especial.Vou tentar elaborar um pouco o número 3, sem entrar na teoria das categorias, se possível.
Cálculos padrão
Todos os cálculos em uma linguagem de programação funcional pode ser visto como funções com um tipo de fonte e de um tipo de destino:
f :: a -> b
. Se uma função tiver mais de um argumento, podemos convertê-la em uma função de um argumento fazendo um curry (consulte também o wiki do Haskell ). E se temos apenas um valorx :: a
(uma função com 0 argumentos), podemos convertê-lo em uma função que leva um argumento do tipo de unidade :(\_ -> x) :: () -> a
.Podemos construir programas mais complexos, a partir de programas mais simples, compondo essas funções usando o
.
operador. Por exemplo, se temosf :: a -> b
eg :: b -> c
recebemosg . f :: a -> c
. Observe que isso também funciona para nossos valores convertidos: se o tivermosx :: a
e o convertermos em nossa representação, obteremosf . ((\_ -> x) :: () -> a) :: () -> b
.Essa representação possui algumas propriedades muito importantes, a saber:
id :: a -> a
para cada tipoa
. É um elemento de identidade com relação a.
:f
é igual af . id
e aid . f
..
é associativo .Cálculos monádicos
Suponha que desejemos selecionar e trabalhar com alguma categoria especial de cálculos, cujo resultado contenha algo além do valor de retorno único. Não queremos especificar o que "algo a mais" significa, queremos manter as coisas o mais geral possível. A maneira mais geral de representar "algo a mais" é representá-la como uma função de tipo - um tipo
m
de tipo* -> *
(ou seja, converte um tipo para outro). Portanto, para cada categoria de computação com a qual queremos trabalhar, teremos alguma função de tipom :: * -> *
. (Em Haskell,m
é[]
,IO
,Maybe
, etc.) E a vontade categoria contém todas as funções de tiposa -> m b
.Agora gostaríamos de trabalhar com as funções nessa categoria da mesma maneira que no caso básico. Queremos poder compor essas funções, queremos que a composição seja associativa e queremos ter uma identidade. Nós precisamos:
<=<
) que compõe funçõesf :: a -> m b
eg :: b -> m c
em algo comog <=< f :: a -> m c
. E, deve ser associativo.return
. Também queremos quef <=< return
seja igualf
e igual areturn <=< f
.Qualquer um
m :: * -> *
para o qual tenhamos tais funçõesreturn
e<=<
seja chamado de mônada . Ele nos permite criar cálculos complexos a partir de cálculos mais simples, como no caso básico, mas agora os tipos de valores de retorno são transformadosm
.(Na verdade, abusei levemente do termo categoria aqui. No sentido da teoria da categoria, podemos chamar nossa construção de categoria somente depois que soubermos que ela obedece a essas leis.)
Mônadas em Haskell
Em Haskell (e outras linguagens funcionais), trabalhamos principalmente com valores, não com funções de tipos
() -> a
. Então, em vez de definir<=<
para cada mônada, definimos uma função(>>=) :: m a -> (a -> m b) -> m b
. Essa definição alternativa é equivalente, podemos expressar>>=
usando<=<
e vice-versa (tente como um exercício ou veja as fontes ). O princípio é menos óbvio agora, mas permanece o mesmo: nossos resultados são sempre de tiposm a
e compomos funções de tiposa -> m b
.Para cada mônada que criamos, não devemos esquecer de verificar isso
return
e<=<
ter as propriedades necessárias: associatividade e identidade esquerda / direita. Expressa usandoreturn
e>>=
eles são chamados de leis de mônada .Um exemplo - listas
Se optarmos
m
por ser[]
, obteremos uma categoria de funções dos tiposa -> [b]
. Tais funções representam cálculos não determinísticos, cujos resultados podem ser um ou mais valores, mas também nenhum valor. Isso dá origem à chamada mônada da lista . A composiçãof :: a -> [b]
eg :: b -> [c]
funciona da seguinte maneira:g <=< f :: a -> [c]
significa calcular todos os resultados possíveis do tipo[b]
, aplicarg
a cada um deles e coletar todos os resultados em uma única lista. Expressado em Haskellou usando
>>=
Observe que neste exemplo os tipos de retorno eram,
[a]
portanto, era possível que eles não contivessem nenhum valor do tipoa
. De fato, não existe um requisito para uma mônada de que o tipo de retorno tenha esses valores. Algumas mônadas sempre têm (gostamIO
ouState
), mas outras não, gostam[]
ouMaybe
.Mônada IO
Como mencionei, a
IO
mônada é um tanto especial. Um valor do tipoIO a
significa um valor do tipoa
construído pela interação com o ambiente do programa. Portanto (ao contrário de todas as outras mônadas), não podemos descrever um valor do tipoIO a
usando alguma construção pura. AquiIO
está simplesmente uma etiqueta ou rótulo que distingue os cálculos que interagem com o ambiente. Este é (o único caso) em que as visualizações 1 e 2 estão corretas.Para a
IO
mônada:f :: a -> IO b
eg :: b -> IO c
meios: Calculef
que interage com o ambiente e, em seguida, calculeg
que usa o valor e calcula o resultado interagindo com o ambiente.return
apenas adiciona aIO
"tag" ao valor (simplesmente "calculamos" o resultado mantendo o ambiente intacto).Algumas notas:
m a
, não há como "escapar" daIO
mônada. O significado é: depois que um cálculo interage com o ambiente, você não pode construir um cálculo que não o faça.IO
mônada. É por isso queIO
costuma ser chamado de bin do pecado de um programador .getChar
devem ter um tipo de resultadoIO something
.fonte
IO
não tem semântica especial do ponto de vista da linguagem. É não especial, ele se comporta como qualquer outro código. Somente a implementação da biblioteca de tempo de execução é especial. Além disso, existe uma maneira de finalidade especial de escape (unsafePerformIO
). Eu acho que isso é importante porque as pessoas geralmente pensamIO
como um elemento de linguagem especial ou uma etiqueta declarativa. Não é.coerce :: a -> b
que converte dois tipos (e trava seu programa na maioria dos casos). Veja este exemplo - você pode converter até uma função emInt
etc.runST :: (forall s. GHC.ST.ST s a) -> a
Vista 1: Mônada como um rótulo
"Conseqüentemente, esse valor Int foi marcado como um valor proveniente de um processo com IO, portanto, esse valor é" sujo "."
"IO Int" geralmente não é um valor Int (embora possa ser, em alguns casos, como "return 3"). É um procedimento que gera algum valor Int. Execuções diferentes deste "procedimento" podem gerar valores Int diferentes.
Uma mônada m é uma "linguagem de programação" incorporada (imperativa): dentro dessa linguagem é possível definir alguns "procedimentos". Um valor monádico (do tipo ma) é um procedimento nesta "linguagem de programação" que gera um valor do tipo a.
Por exemplo:
é um procedimento que gera um valor do tipo Int.
Então:
é um procedimento que gera duas Ints (possivelmente diferentes).
Cada "linguagem" desse tipo suporta algumas operações:
dois procedimentos (ma e mb) podem ser "concatenados": você pode criar um procedimento maior (ma >> mb) feito do primeiro e depois do segundo;
além do mais, a saída (a) da primeira pode afetar a segunda (ma >> = \ a -> ...);
um procedimento (retorno x) pode render algum valor constante (x).
As diferentes linguagens de programação incorporadas diferem quanto ao tipo de suporte, como:
fonte
Não confunda um tipo monádico com a classe monad.
Um tipo monádico (isto é, um tipo sendo uma instância da classe monad) resolveria um problema específico (em princípio, cada tipo monádico resolve um problema diferente): Estado, Aleatório, Talvez, E / S. Todos eles são tipos com contexto (o que você chama de "rótulo", mas não é isso que os torna uma mônada).
Para todos eles, há a necessidade de "encadear operações com opção" (uma operação depende do resultado da anterior). Aqui entra em jogo a classe mônada: faça com que seu tipo (resolvendo um determinado problema) seja uma instância da classe mônada e o problema de encadeamento seja resolvido.
Consulte O que a classe da mônada resolve?
fonte