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:

Valor e contexto

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 como fmap 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, como Just 2
3. e retorna um novo Functor, como Just 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 Justs e Nothings:

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:

  1. Consiga um doutorado em Ciência da Computação.
  2. 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:

Apenas uma monad relaxando

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, como Just 3
2. e uma função que retorna uma Monad (como a função half)
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 um Just ou um Nothing)

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

  1. Um functor é um tipo que implementa Functor typeclass.
  2. Um applicative é um tipo que implementa Applicative typeclass.
  3. Uma monad é um tipo que implementa Monad typeclass.
  4. 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 <*> or liftA
  • monads: aplicam uma função que retorna um valor envolvido em um valor envolvido usando >>= or liftM

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.