Anotações do Hercules

Entendendo o que é monad implementando uma monad

18-05-2015

Antes de começar a falar sobre monads, um aviso. Tentarei explicar de forma mais abstrata, porém, como irei usar Haskell nas implementações dos exemplos alguns detalhes serão específicos desta linguagem. Ficarei feliz em ajudar a implementar estes exemplos abaixo em outras linguagens caso haja interesse, mas irei me ater usando Haskell neste post.

O que é uma monad?

Monad é uma estrutura que representa computações como sequência de passos. Hmm… muito abstrato, não é?

Simplificando um pouco, monad é um valor com contexto. Bom, mas o que seria esse contexto?

Vamos criar um tipo chamado Talvez como exemplo, com a seguinte implementação:

data Talvez a = Nada | Apenas a deriving (Show)

Como podem ver, o tipo Talvez tem dois construtores de tipo, o Nada e o Apenas. O tipo a no construtor quer dizer que qualquer tipo de valor é aceito ao criar um valor do tipo Talvez. Vamos ao GHCI (REPL do Haskell) verificar como podemos usar esse tipo:

ghci> Nada
Nada
ghci> Apenas 1
Apenas 1
ghci> Apenas "uma string"
Apenas "uma string"

Repare no tipo de Nada e Apenas inferidos no REPL:

ghci> :type Nada
Nada :: Talvez a
ghci> :type Apenas 1
Apenas 1 :: Num a => Talvez a
ghci> :type Apenas "uma string"
Apenas "uma string" :: Talvez [Char]

O tipo Talvez a representa uma valor do tipo a com um possível contexto de falha ou sucesso. O valor Apenas 1 indica que há um número, já Nada indica uma ausência de valor. Se você considerar esses valores como resultados de computações, o Nada indica que a computação falhou, enquanto que o Apenas 1 foi uma computação bem sucedida.

Ok! Mas apenas isso não torna o tipo Talvez uma monad. Para isso, temos de tornar nosso tipo uma instância de Monad. Há também algumas leis a serem respeitadas, mas vamos ignorar isso por enquanto.

Temos de fazer com que Talvez implemente a type class Monad, que exige que implementemos as funções return e »=(pronunciada como bind).

Implementando a type class Monad

import Control.Monad

data Talvez a = Nada | Apenas a deriving (Show)

instance Monad Talvez where
  return :: a -> Talvez a
  (>>=) :: Talvez a -> (a -> Talvez b) -> Talvez b

Vamos verificar as assinatura de tipos das funções, para entender o que cada função deve fazer.

Para a função return, dado um valor do tipo a(pode ser qualquer tipo, como Int, String, Bool, etc), este valor deve ser inserido em um contexto mínimo(no nosso caso o Apenas, já que o Nada significa ausência de valor). Só lembrando que, a função return aqui nada tem a ver com return usado em outras linguagens de programação.

A implementação ficará assim:

return :: a -> Talvez a
return a = Apenas a

A assinatura da função »= diz que, dado um valor do tipo Talvez como primeiro parâmetro, e uma função(que recebe um valor a e retorna um valor do tipo Talvez a) como segundo parâmetro, retorna um tipo Tavez b como resultado.

A implementação:

(>>=) :: Talvez a -> (a -> Talvez b) -> Talvez b
Nada >>= f = Nada
(Apenas a) >>= f = f a

Se você não está acostumado com código em Haskell, não se assuste. Para implementar a função »= foi utilizado aqui a notação infixa e pattern matching. O valor a esquerda de »= é o valor do primeiro parâmetro, a direita é a função passada como segundo parâmetro.

A função »= funciona então da seguinte maneira: quando o primeiro parâmetro for Nada, retorne Nada sem aplicar a função, quando for Apenas a, aplique a função ao valor a. Lembrando que a função a ser aplicada deve receber um valor qualquer e retornar um Talvez b.

Vamos criar algumas funções de exemplo e experimentar as implementações:

talvezSoma1 :: Int -> Talvez Int
talvezSoma1 numero = Apenas (numero + 1)

somaNada :: Int -> Talvez Int
somaNada _ = Nada

ghci> Apenas 1 >>= talvezSoma1
Apenas 2
ghci> Apenas 5 >>= talvezSoma1
Apenas 6
ghci> Apenas 1 >>= somaNada
Nada
ghci> Nada >>= talvezSoma1
Nada
ghci> Apenas 1 >>= talvezSoma1 >>= talvezSoma1
Apenas 3
ghci> Nada >>= talvezSoma1 >>= talvezSoma1
Nada
ghci> Apenas 1 >>= talvezSoma1 >>= somaNada >>= talvezSoma1
Nada

Podemos encadear a chamada de várias funções desta maneira. Bacana não é?

Bom, ainda não acabamos. Só porque tornamos o nosso tipo uma instância de Monad ainda não o torna uma. Para isso precisamos obedecer algumas leis.

As leis a serem seguidas

Vamos ver cada uma e conferir se Talvez a está obedecendo a cada uma:

  • A primeira lei define que, se pegarmos um valor, colocarmos em um contexto mínimo usando a função return e então passar para função »=, o resultado será o mesmo que pegar o valor e aplicar a função.
ghci> talvezSoma1 1
Apenas 2
ghci> return 1 >>= talvezSoma1
Apenas 2
  • A segunda lei define que, se temos um valor que é uma monad, e nos usarmos a função »= para passar o valor para a função return, o resultado deve ser o nosso valor original.
ghci> Apenas 1 >>= return
Apenas 1
  • A terceira e última lei diz que, dado um encadeamento de chamadas de função, a ordem que as mesmas são aninhadas não deve influenciar no resultado.
ghci> Apenas 1 >>= (\n -> Apenas (n + 1) >>= talvezSoma1)
Apenas 4
ghci> (Apenas 1 >>= \n -> Apenas (n + 1)) >>= talvezSoma1
Apenas 4
ghci> Apenas 1 >>= (somaNada >>= talvezSoma1)
Nada
ghci> (Apenas 1 >>= somaNada) >>= talvezSoma1
Nada

Como pudemos ver nos exemplos, nossa implementação obedece a todas as leis. Devemos lembrar de seguir essas leis ao implementar uma monad, pois o compilador não irá nos alertar a respeito disso.

Acabamos de reimplementar a monad Maybe a que existe na biblioteca padrão do Haskell. Não em sua totalidade é verdade, mas o suficiente para entender como implementar uma monad. Vá em frente, substitua nos códigos de exemplo o Talvez por Maybe, Nada por Nothing, Apenas por Just e verá que o comportamento é o mesmo. :-)

Recapitulando

Uma monad é um tipo de computação que carrega consigo um contexto, implementa type class Monad, e segue as 3 leis estabelecidas para monads.

A biblioteca padrão do Haskell tem outras monads, e como pudemos ver nesses exemplos, monads não necessariamente precisam causar efeitos colaterais, mas a forma que Haskell usa para causar efeitos colaterais é através de monads. Há também um sintax sugar para lidar com monads, mas esses tópicos ficarão para um próximo post.

Deixo aqui os agradecimentos a @claytonsilva e @jugoncalves pela revisão do texto.

Monad (Wikipedia)

Monad laws (Learn you a Haskell)

comments powered by Disqus