As visualizações 1 e 2 estão incorretas em geral.
- Qualquer tipo de dado
* -> *
pode funcionar como um rótulo; as mônadas são muito mais que isso.
- (Com exceção da
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 valor x :: 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 temos f :: a -> b
e g :: b -> c
recebemos g . f :: a -> c
. Observe que isso também funciona para nossos valores convertidos: se o tivermos x :: a
e o convertermos em nossa representação, obteremos f . ((\_ -> x) :: () -> a) :: () -> b
.
Essa representação possui algumas propriedades muito importantes, a saber:
- Temos uma função muito especial - a função de identidade
id :: a -> a
para cada tipo a
. É um elemento de identidade com relação a .
: f
é igual a f . id
e a id . f
.
- O operador de composição da função
.
é 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 tipo m :: * -> *
. (Em Haskell, m
é []
, IO
, Maybe
, etc.) E a vontade categoria contém todas as funções de tipos a -> 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:
- Ter um operador (vamos chamá-lo
<=<
) que compõe funções f :: a -> m b
e g :: b -> m c
em algo como g <=< f :: a -> m c
. E, deve ser associativo.
- Para ter alguma função de identidade para cada tipo, vamos chamá-lo
return
. Também queremos que f <=< return
seja igual f
e igual a return <=< f
.
Qualquer um m :: * -> *
para o qual tenhamos tais funções return
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 transformados m
.
(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 tipos m a
e compomos funções de tipos a -> 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 usando return
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 tipos a -> [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ção f :: a -> [b]
e g :: b -> [c]
funciona da seguinte maneira: g <=< f :: a -> [c]
significa calcular todos os resultados possíveis do tipo [b]
, aplicar g
a cada um deles e coletar todos os resultados em uma única lista. Expressado em Haskell
return :: a -> [a]
return x = [x]
(<=<) :: (b -> [c]) -> (a -> [b]) -> (a -> [c])
g (<=<) f = concat . map g . f
ou usando >>=
(>>=) :: [a] -> (a -> [b]) -> [b]
x >>= f = concat (map f x)
Observe que neste exemplo os tipos de retorno eram, [a]
portanto, era possível que eles não contivessem nenhum valor do tipo a
. 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 (gostam IO
ou State
), mas outras não, gostam []
ou Maybe
.
Mônada IO
Como mencionei, a IO
mônada é um tanto especial. Um valor do tipo IO a
significa um valor do tipo a
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 tipo IO a
usando alguma construção pura. Aqui IO
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:
- Composição
f :: a -> IO b
e g :: b -> IO c
meios: Calcule f
que interage com o ambiente e, em seguida, calcule g
que usa o valor e calcula o resultado interagindo com o ambiente.
return
apenas adiciona a IO
"tag" ao valor (simplesmente "calculamos" o resultado mantendo o ambiente intacto).
- As leis da mônada (associatividade, identidade) são garantidas pelo compilador.
Algumas notas:
- Como os cálculos monádicos sempre têm o tipo de resultado
m a
, não há como "escapar" da IO
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.
- Quando um programador funcional não sabe como fazer algo de uma maneira pura, ele pode (como último recurso) programar a tarefa por algum cálculo de estado dentro da
IO
mônada. É por isso que IO
costuma ser chamado de bin do pecado de um programador .
- Observe que em um mundo impuro (no sentido de programação funcional) a leitura de um valor também pode mudar o ambiente (como consumir a entrada do usuário). É por isso que funções como
getChar
devem ter um tipo de resultado IO something
.