Functors, Applicatives e Monads explicados com desenhos
Se você trabalha desenvolvendo software, deve conhecer a pergunta do momento: O que são Monads? A resposta mais didática e divertida que encontrei está no post Functors, Applicatives, And Monads In Pictures. Há traduções desse post em várias línguas, então decidi fazer a tradução para português.
Esse é um valor simples:
E sabemos como aplicar uma função nesse valor:
Bem simples. Vamos expandir esse exemplo dizendo que qualquer valor pode estar em um contexto. Por enquanto, pense no contexto como uma caixa onde colocamos um valor dentro:
Agora quando aplicarmos uma função nesse valor, teremos resultados diferentes dependendo do contexto. Essa é a idéia na qual Functors, Applicatives, Monads, Arrows e etc são baseados. O tipo Maybe
(que significa Talvez) define dois contextos relacionados: Nothing
(Nada) e Just a
(que significa "Simplesmente a" onde a
é um elemento que nós definimos, como um número ou uma String):
data Maybe a = Nothing | Just a
Veremos em um segundo como aplicar funções é diferente quando algo é um Just a
ou um Nothing
. Mas primeiro, vamos falar de Functors!
Functors
Quando um valor está envolvido por um contexto, você não pode aplicar uma função comum nele:
Aqui é onde fmap
entra. fmap
é malandro, fmap
manja dos paranauês. fmap
sabe como aplicar funções em valores que estão envolvidos por contextos. Como exemplo, vamos aplicar (+3)
a Just 2
. Usando fmap
:
> fmap (+3) (Just 2)
Just 5
Rá! fmap
mostrou como se faz! Mas comofmap
sabe como aplicar a função?
Na real, o que é um Functor?
Functor
é uma typeclass. Essa é a definição:
1. Para fazer do tipo ‘f’ um Functor,
2. Esse tipo precisa definir comofmap
vai trabalhar com ele
Um Functor
é qualquer tipo que define como aplicar fmap
a ele mesmo. É assim quefmap
funciona:
1.
fmap
recebe uma função, como(+3)
2. e um Functor, comoJust 2
3. e retorna um novo Functor, comoJust 5
De forma que podemos fazer isto:
> fmap (+3) (Just 2)
Just 5
E fmap
magicamente aplica a função porque Maybe
é um Functor. Ele especifica como aplicar fmap
em Just
s e Nothing
s:
instance Functor Maybe where
fmap func (Just val) = Just (func val)
fmap func Nothing = Nothing
Isso é o que acontece por baixo dos panos quando escrevemos
fmap (+3) (Just 2)
1. Extrai valor do contexto
2. Aplica a função
3. Envolve o valor em um contexto novamente
Daí você vira e manda, seguinte fmap
, tem como aplicar (+3)
aí noNothing
?
1. Não há valor
2. Não aplica a função
3. Acaba sem nada
> fmap (+3) Nothing
Nothing
Assim como Morpheus em Matrix, fmap
sabe o que tem que fazer; começa comNothing
, e termina comNothing
! fmap
é zen. Agora a existência do tipo Maybe
faz sentido. Por exemplo, é assim que se trabalha com registros em base de dados numa linguagem que não possui Maybe
:
post = Post.find_by_id(1)
if post
return post.title
else
return nil
end
Mas em Haskell:
fmap (getPostTitle) (findPost 1)
Se findPost
retorna um post, nós obtemos o título comgetPostTitle
. Se ele retornarNothing
, nós retornamosNothing
! Maneiro, hein? <$>
é a versão infixa defmap
, de forma que podemos usá-lo assim:
getPostTitle <$> (findPost 1)
Outro exemplo: o que acontece quando aplicamos a função numa lista?
1. Um array de valores
2. Aplica a função em cada valor
3. Um novo array de valores
Listas também são functors! Essa é a definição:
instance Functor [] where
fmap = map
Certo, certo, último exemplo: o que acontece quando aplicamos uma função em outra função?
fmap (+3) (+1)
Essa é uma função:
1. Recebe um valor
2. Retorna um valor
Essa é uma função sendo aplicada a outra função:
O resultado é simplesmente outra função!
> import Control.Applicative
> let foo = fmap (+3) (+2)
> foo 10
15
Então funções também são Functors!
instance Functor ((->) r) where
fmap f g = f . g
Quando aplicamos fmap
em outra função, estamos fazendo composição de funções!
Applicatives
Applicatives nos leva para o próximo level. Com um applicative, nossos valores são envolvidos por um contexto, exatamente como Functors:
Mas nossas funções são envolvidas por um contexto também!
Eu sei. Aceita, só aceita. Applicatives não estão de brincadeira. Control.Applicative
define <*>
, que sabe como aplicar uma função envolvida por um contexto a um valor envolvido por um contexto:
1. Função envolvida por um contexto
2. Valor envolvido por um contexto
3. Retira ambos dos contextos e aplica a função ao valor
4. Novo valor em um novo contexto
Just (+3) <*> Just 2 == Just 5
Usar <*>
pode levar a situações interessantes. Por exemplo:
> [(*2), (+3)] <*> [1, 2, 3]
[2, 4, 6, 4, 5, 6]
Isso é algo que pode ser feito com Applicatives que não pode ser feito com Functors. Como se aplica uma função que recebe dois argumentos em dois valores envoltos por contextos?
> (+) <$> (Just 5)
Just (+5)
> Just (+5) <$> (Just 4)
ERRO??? COMO ASSIM??? POR QUÊ A FUNÇÃO ESTÁ ENVOLVIDA POR UM JUST???
Applicatives:
> (+) <$> (Just 5)
Just (+5)
> Just (+5) <*> (Just 3)
Just 8
Applicatives
colocam umFunctors
de lado. “Somente adultos podem usar funções com qualquer número de argumentos, garoto” dizem os Applicatives. “Armado com <$>
e <*>
, posso lidar com funções que recebem qualquer número de valores sem contextos. Daí eu passo todos os valores envolvidos e recebo um valor envolvido como retorno! HUEHUEHUE!”
> (*) <$> Just 5 <*> Just 3
Just 15
E olha! Tem uma função chamadaliftA2
que faz a mesma coisa:
> liftA2 (*) (Just 5) (Just 3)
Just 15
Monads
Como aprender Monads:
- Consiga um doutorado em Ciência da Computação.
- Joga fora porque não será necessário para entender essa seção!
Monads adicionam um toque especial.
Functors aplicam funções em valores envolvidos:
Applicatives aplicam uma função envolvida em um valor envolvido:
Monads aplicam uma função que retorna um valor envolvido em um valor envolvido. Monads possuem uma função>>=
(chamada “bind”) que faz isso.
Vamos ver um exemplo. Nossa amigaMaybe
é uma monad:
Supondo que half
é uma função que trabalha apenas com números pares:
half x = if even x
then Just (x `div` 2)
else Nothing
- Recebe um valor
- Retorna um valor envolvido
O que acontece se passarmos um valor envolvido?
Precisamos usar >>=
para empurrar nosso valor envolvido na função. Aqui temos uma foto do >>=
:
Funciona assim:
> Just 3 >>= half
Nothing
> Just 4 >>= half
Just 2
> Nothing >>= half
Nothing
O que acontece por baixo dos panos? Monad
é outra typeclass. Essa é uma definição parcial:
class Monad m where
(>>=) :: m a -> (a -> m b) -> m b
Onde >>=
é:
1.
>>=
recebe una Monad, comoJust 3
2. e uma função que retorna uma Monad (como a funçãohalf
)
3. e retorna uma Monad
Então Maybe
é uma Monad:
instance Monad Maybe where
Nothing >>= func = Nothing
Just val >>= func = func val
Vamos vê-la em ação comJust 3
!
1. Bind extrai o valor
2. Passa o valor extraído para a função
3. Retorna um valor envolvido (que pode ser umJust
ou umNothing
)
E se passamos um Nothing
é ainda mais simples:
1. Nada entra
2. Não faz nada
3. Nada sai
Também podemos encadear essas chamadas:
> Just 20 >>= half >>= half >>= half
Nothing
Coisa linda! Agora sabemos queMaybe
é umFunctor
, um Applicative
, e umaMonad
.
Agora vamos dar um rolêzinho em outro exemplo: a IO
monad (de input/output = Entrada/Saída em inglês):
Mais especificamente três funções. getLine
não recebe argumentos e lê a entrada de dados do usuário:
getLine :: IO String
readFile
recebe uma string (o nome de um arquivo) e retorna o conteúdo daquele arquivo:
readFile :: FilePath -> IO String
putStrLn
recebe uma string e a imprime:
putStrLn :: String -> IO ()
As três funções recebem valores comuns (ou nenhum valor) e retornam um valor envolvido. Podemos encadear todas elas com >>=
!
1. Lê os dados do usuário
2. Usa-os para ler um arquivo
3. Imprime o conteúdo do arquivo!
getLine >>= readFile >>= putStrLn
Oh yeah! Ótimo lugar para assistir o espetáculo das Monads!
Haskell também vem com um açúcar sintático para monads, chamado do-notation
:
foo = do
filename <- getLine
contents <- readFile filename
putStrLn contents
Conclusão
- Um functor é um tipo que implementa
Functor
typeclass. - Um applicative é um tipo que implementa
Applicative
typeclass. - Uma monad é um tipo que implementa
Monad
typeclass. - Um
Maybe
implementa todos os três, então é um functor, um applicative, e uma monad.
E qual é a diferença entre os três?
- functors: aplicam uma função em um valor envolvido usando
fmap
ou<$>
- applicatives: aplicam uma função envolvida em um valor envolvido usando
<*>
orliftA
- monads: aplicam uma função que retorna um valor envolvido em um valor envolvido usando
>>=
orliftM
Então, querido amigo(creio que somos amigos nesse ponto), acho que nós dois concordamos que monads são simples e UMA BOA IDÉIA®. Agora que você molhou o bico com esse guia, por quê não beber a garrafa toda? Dê uma olhada na seção de Monads do “Learn you a Haskell for Great Good”. Há vários tópicos que eu apenas passei rapidamente porque a autora, Miran Lipovaca fez um grande trabalho indo fundo nesse assunto.