Você Poderia Ter Inventado Mônadas! (E Talvez Já O Tenha)
Table of Contents
- 1. INTRO
- 2. Efeitos Colaterais: Depurando Funções Puras
- 3. Um Contêiner: Funções Multi-Valoradas
- 4. Um Efeito Colateral Mais Complexo: Números Aleatórios
- 5. Mônadas
- 6. Sintaxe da Mônada
- 7. Entrada&Saída
- 8. Teoria das Categorias
- 9. Apêndice: Um Exemplo de Código Completo Usando Números Aleatórios
- 10. META
1 INTRO
Se você ainda não sacou, isto é sobre mônadas como elas aparecem em linguagens de programação funcional pura como Haskell. Elas são fortemente relacionadas com as mônadas da teoria de categorias, mas não são exatamente as mesmas porque Haskell não garante as identidades satisfeitas pelas mônadas categóricas.
Escrever introduções sobre mônadas parecem ter-se tornado uma indústria. Existe uma introdução gentil, uma introdução para programadores Haskell com os dizeres "Não entre em pânico", uma introdução para o "programador Haskell em produção" e infindáveis outras que introduzem mônadas como tudo desde um funtor até um tipo de traje espacial. 1
Mas todas elas introduzem mônadas como algo esotérico que necessita de explicação. Mas o que quero argumentar é que elas não são esotéricas afinal. De fato, diante de muitos problemas na programação funcional você já foi levado, inexoravelmente, a certas soluções, todas elas exemplos de mônadas. De fato, eu espero conseguir que você invente-as agora se é que já não as inventou. Será então um pequeno passo notar que todas essas soluções são de fato a mesma solução disfarçada. E depois de ler isto, você pode estar em uma posição melhor para entender outros documentos sobre mônadas porque você reconhecerá tudo que vê como algo que já inventou.
Muitos dos problemas que mônadas tentam resolver são relacionados com a questão dos efeitos colaterais. Então nós começaremos com eles. (Note que mônadas te permitem fazer mais que manipular efeitos colaterais, em particular muitos tipos de objetos-contêiner podem ser vistos como mônadas. Algumas das introduções a mônadas creem ser difícil reconciliar estes dois usos distintos das mônadas e concentram-se em apenas um ou outro.)
2 Efeitos Colaterais: Depurando Funções Puras
Numa linguagem imperativa como C++, funções não agem nada parecido com as funções da matemática. Por exemplo, suponha que tenhamos uma função C++ que toma um simples argumento em ponto flutuante e retorna um resultado em ponto flutuante. Superficialmente ela pode parecer uma pequena função matemática mapeando reais em reais, mas uma função C++ pode fazer mais que apenas retornar um número que depende de seus argumentos. Ela pode ler e escrever valores em variáveis globais bem como escrever saída para a tela e receber entrada do usuário. Numa linguagem funcional pura, porém, uma função só pode ler o que é suprido em seus argumentos e a única maneira pela qual ela pode ter um efeito no mundo é mediante o valor que retorna.
Então, considere este problema numa linguagem funcional pura: temos funções f
e g
que levam float
s em float
s, mas gostaríamos de modificar estas funções
para que também emitam strings para fins de depuração. Em Haskell, f
e g
podem ter tipos dados por
f,g :: Float -> Float
Como podemos modificar os tipos de f
e g
para admitirem efeitos colaterais?
Bem, realmente não existe escolha afinal. Se gostaríamos de ter f'
e g'
produzindo strings bem como números de ponto flutuante como saída, então a única
forma possível é que estas strings sejam retornadas junto aos números de ponto
flutuante. Em outras palavras, precisamos que f'
e g'
sejam do tipo
f',g' :: Float -> (Float,String)
Podemos desenhar isso diagramaticamente como
x
|
+---+
| f'|
+---+
| \
| |
f x "f was called."
Podemos pensar nelas como 'funções depuráveis'.
Mas suponha agora que gostaríamos de depurar a composição de duas funções desse
tipo. Nós poderíamos simplesmente compor nossas funções originais, f
e g
,
para formar f . g
. Mas nossas funções depuráveis não são tão imediatadas de se
lidar. Nós gostaríamos que as strings retornadas por f'
e g'
fossem
concatenadas numa grande string de depuração ( a de f'
após a de g'
). Mas não
podemos simplesmente compor f'
e g'
porque o valor de retorno de g'
não é
do mesmo tipo que o argumento de f'
. Nós poderíamos escrever código num estilo
como esse:
let (y,s) = g' x (z,t) = f' y in (z,s++t)
Eis a aparência diagramática disso:
x | +---+ | g'| +---+ | \ +---+ | "g was called." | f'| | +---+ | | \ | | \ | | +----+ | | ++ | | +----+ | | f (g x) "g was called.f was called."
Isso é uma trabalheira toda vez que se precisa compor duas funções depuráveis, e
se tivermos que implementar esse tipo de serviço de baixo nível 2 ao longo
de todo o código, seria um porre. O que precisamos é definir uma função de ordem
superior para realizar este serviço para nós. Como o problema é que a saída de
g'
não pode ser simplesmente inserida na entrada de f'
, precisamos
'melhorar' f'
. Então introduziremos uma função `bind` para fazer isso. Em
outras palavras, queremos
bind f' :: (Float,String) -> (Float,String)
que implique em
bind :: (Float -> (Float,String)) -> ((Float,String) -> (Float,String))
`bind` deve servir a dois propósitos:
- aplicar
f'
na parte correta deg' x
, e - concatenar a string retirnada por
g'
com a string retornada porf'
.
Exercício 1
Escreva a função bind
.
Solução
bind f' (gx,gs) = let (fx,fs) = f' gx in (fx,gs++fs)
Dado um par de funções depuráveis, f'
e g'
, agora podemos compô-las juntas
para fazer uma nova função depurável bind f' . g'
. Escreva essa
composição como f' * g'
. Mesmo que a saída de g'
seja incompatível com a
entrada de f'
, nós ainda temos uma maneira fácil de concatenar suas
operações. E isto sugere outra questão: existe uma função depurável
'identidade'? A função identidade ordinária tem as seguintes propriedades:
f . id = f
e id . f = f
. Assim, estamos procurando uma função depurável,
chamemo-la unidade (unit
), tal que unit * f = f * unit = f
. Obviamente
esperamos que ela produza a string de depuração vazia e em outros casos aja como
a identidade.
Exercício 2
Defina a unidade.
Solução
unit x = (x,"")
A unidade nos permite 'elevar' qualquer função em uma depurável. De fato, defina
lift f x = (f x,"")
ou mais simplesmente lift f = unit . f
. A versão elevada faz
basicamente o mesmo da original e, bem razoavelmente, produz a string vazia como
efeito colateral.
Exercício 3
Mostre que lift f * lift g = lift (f.g)
Em suma: as funções bind
e unit
nos permitem compor funções depuráveis de
maneira direta, e compor funções ordinárias com funções depuráveis de uma
maneira natural.
Acredite ou não, fazendo estes dois exercícios você já definiu sua primeira
mônada. Neste ponto, provavelmente não está claro qual das estruturas que
observamos é a mônada em si, ou com o que outras mônadas se parecem. Mas em vez
de definir mônadas agora eu vou te deixar com mais alguns exercícios fáceis que
te introduzirão a outras mônadas de tal maneira que você verá por si mesmo que
esta é uma estrutura comum digna de um nome próprio. Também estou confiante que
a maioria das pessoas, quando de frente com o problema original, eventualmente
chegarão à função bind
como uma forma de colar suas funções depuráveis. Então
estou certo que você poderia ter inventado esta mônada mesmo que não notasse que
era uma mônada.
3 Um Contêiner: Funções Multi-Valoradas
Considere as funções sqrt
e cbrt
que computam as raízes quadrada e cúbica,
respectivamente, de um número real. Elas são funções comuns do tipo
Float -> Float
(apesar de que sqrt
atirará uma exceção para
argumentos negativos, algo que ignoraremos).
Agora considere uma versão dessas funções que trabalhe com números
complexos. Cada complexo, além do zero, tem duas raízes quadradas. Da mesma
forma, cada complexo não-nulo tem três raízes cúbicas. Então gostaríamos que
sqrt'
e cbrt'
retornassem listas de valores. Em outras palavras, queremos
sqrt',cbrt' :: Complex Float -> [Complex Float]
Vamos chamá-las de funções multi-valoradas.
Suponha que queremos determinar a raiz sexta de um número real. Nós podemos
simplesmente concatenar as funções de raiz quadrada e raiz cúbica. Em outras
palavras podemos definir sixthroot x = sqrt (cbrt x)
.
Mas como definimos uma função que encontra todas as raízes sextas de um número
complexo usando sqrt'
e cbrt'
? Não podemos simplesmente concatenar estas
funções. O que queremos é primeiro computar as raízes cúbicas de um número, e
então encontrar as raízes quadradas de cada um destes números por vez,
combinando os resultados em uma lista maior. O que precisamos é de uma função,
vamos chamá-la bind
, para compor estas funções, com declaração
bind :: (Complex Double -> [Complex Double]) -> ([Complex Double] -> [Complex Double])
Eis um diagrama mostrando como é o processo. Nós queremos escrever cbrt'
apenas uma vez mas ainda assim tê-lo aplicado a ambos os valores de sqrt'
.
64 | +------+ + sqrt'+ +------+ +8 / \ -8 +------+ +------+ | cbrt'| | cbrt'| +------+ +------+ | | | | | | 2 . . -2 . .
Exercício 4
Escreva uma implementação para bind
.
Solução
bind f x = concat (map f x)
Como escrevemos a função identidade na forma multi-valorada? A identidade retorna
um argumento, então uma versão multi-valorada deve retornar uma lista de
tamanho 1. Chamemos esta função unit
.
Exercício 5
Defina unit
.
Solução
unit x = [x]
Novamente, defina f * g = bind f . g and lift f = unit . f
. lift
faz exatamente o que você poderia esperar. Ele transforma uma função ordinária
em uma multi-valorada da maneira óbvia.
Exercício Seis
Mostre que f * unit = unit * f = f and lift f * lift g = lift (f.g)
Novamente dado o problema original, somos inexoravelmente levados a esta função
bind
.
Se você lidou com estes exercícios então você definiu sua segunda mônada. Você talvez esteja notando um padrão a se desenvolver. É curioso que estes dois problemas aparentemente tão diferentes tenham levado a construções similares.
4 Um Efeito Colateral Mais Complexo: Números Aleatórios
A função random
em Haskell tem essa forma:
random :: StdGen -> (a,StdGen)
A ideia é que para que se gere um número aleatório é necessário uma semente, e
após ter gerado o número atualizar a semente com um novo valor. Em uma linguagem
impura, a semente pode ser uma variável global, assim o usuário não precisa
lidar diretamente com ela. Mas numa linguagem pura a semente precisa ser passada
e recuperada explicitamente - e é isso que a assinatura de tipo de random
descreve. Note que isto é similar ao caso da depuração acima porque estamos
retornando dados extras usando um par. Mas desta vez estamos recebendo dados
extras também.
Então uma função que é conceitualmente uma função randomizada a ->
b
pode ser escrita como uma função a -> b
a -> StdGen
-> (b,StdGen)
onde StdGen
é o tipo da semente.
Agora deveremos trabalhar como compor duas funções randomizadas, f
e g
. O
primeiro elemento do par que f
retorna precisa ser passado como uma entrada
para g
. Mas a semente passada de g
também precisa ser passada para
f
. Enquanto isso o 'real' valor de retorno de g
precisa ser passado como
primeiro argumento de f
. Assim nós podemos dar esta assinatura para bind
:
bind :: (a -> StdGen -> (b,StdGen)) -> (StdGen -> (a,StdGen)) -> (StdGen -> (b,StdGen))
Exercício 7
Implemente bind
.
Solução
bind f x seed = let (x',seed') = x seed in f x' seed'
Agora precisamos encontrar a função randomizada 'identidade'. Ela precisa ser do tipo
unit :: a -> (StdGen -> (a,StdGen))
e deve deixar a semente intocada.
Exercício 8
Implemente unit
.
Solução
unit x g = (x,g) # ou também unit = (,)
Mais uma vez, defina f * g = bind f . g
e lift f = unit . f
. lift
faz
exatamente o que você poderia esperar - transforma uma função ordinária em uma
randomizada que deixa a semente intocada.
Exercício 9
Mostre que f * unit = unit * f = f
e lift f * lift g = lift (f.g)
.
5 Mônadas
Agora é hora de retornar e discernir a estrutura comum.
Defina
type Debuggable a = (a,String) type Multivalued a = [a] type Randomised a = StdGen -> (a,StdGen)
Use a variável m
para representar Debuggable
, Multivalued
ou
Randomised
. Em cada caso acabamos no mesmo problema. Nos é dada uma função a
-> m b
mas precisamos de alguma maneira aplicar esta função a um objeto do tipo
m a
em vez de um do tipo a
. Em cada caso fazemos isso definindo uma função
de nome bind
com tipo (a -> m b) -> (m a -> m b)
e introduzindo
um tipo de função identidade unit :: a -> m a
. Além disso, nós
descobrimos que estas identidades valem: f * unit = unit * f = f
e
lift f * lift g = lift (f.g)
, onde *
e lift
foram definidas em
termos de unit
e bind
.
Então agora eu posso revelar o que é uma mônada. A tripla de objetos
(m,bind,unit)
é a mônada, e para ser mônada deve satisfazer um punhado de leis
como as que estivemos demonstrando. E eu penso que em cada um destes três casos
você eventualmente viria com uma solução como bind
, mesmo que possa não ter
notado imediatamente que todos esses casos compartilham uma estrutura comum.
Pois agora eu preciso fazer algum contato com a definição usual das mônadas
Haskell. A primeira coisa é que eu troquei a definição de bind
e a escrevi com
o nome 'bind', ao passo que normalmente escrita como o operador >>=
. Então
bind f x
normalmente é escrito como x >>
f=. A segunda
é que unit
geralmente é chamada return
. E em terceiro lugar, a fim de
sobrescrever os nomes >>=
e return
, precisamos fazer uso de classes de
tipo. Em Haskell, Debuggable
é a mônada Writer
, Multivalued
é a mônada
List
e Randomised
é a mônada State
. Se você conferir as definições dessas,
você notará que, excetuando algum floreio sintático, elas são essencialmente as
definições que você escreveu para os exercícios acima. A depuração usou a mônada
Writer
, as funções multi-valoradas usaram a mônada List
, e o gerador de
números aleatórios usou a mônada State
. Você poderia ter inventado mônadas!
6 Sintaxe da Mônada
Não quero perder tempo demais nisso (e você pode saltar esta seção) porque existem muitas excelentes introduções por aí.
Você já viu como a função bind
pode prover uma bela forma de unir funções para
te livrar de escrever um bom tanto de código feio. Haskell vai um passo além,
você nem precisa usar a função bind
explicitamente, você pode fazer que o
Haskell a insira automaticamente em seu código.
Vamos retornar ao exemplo original de depuração, exceto que agora usaremos as
classes de tipo oficiais de Haskell. Onde anteriormente nós usamos um par como
(a,s)
agora usaremos Writer (a,s)
do tipo Writer
Char
. E para recuperar o par de um destes objetos usamos a função
runWriter
. Suponha que desejamos incrementar, duplicar e então decrementar 7,
a cada estágio registrando o que fizemos. Nós podemos escrever
return 7 >>= (\x -> Writer (x+1,"inc.")) >>= (\x -> Writer (2*x,"double.")) >>= (\x -> Writer (x-1,"dec."))
Se aplicarmos runWriter
a isso obteremos (15,"inc.double.dec.")
. Mas ainda
está bem feio. Em vez disso podemos usar a sintaxe do
de Haskell. A ideia é
que
do x <- y mais-codigo-aqui
é automaticamente reescrito pelo compilador para
y >>= (\x -> do mais-codigo-aqui)
Similarmente,
do let x = y mais-codigo-aqui
é reescrito como
(\x -> do mais-codigo-aqui) y
E
do
expression
é deixado somente como expression
.
Podemos agora reescrever o código acima como:
do let x = 7 y <- Writer (x+1,"inc\n") z <- Writer (2*y,"double\n") Writer (z-1,"dec\n")
A notação é bastante sugestiva. Quando escrevemos y <- ...
é como se
fingíssemos que a expressão do lado direito é meramente x+1
e que o efeito
colateral simplesmente cuida de si mesmo.
Outro exemplo. Escreva o exemplo da raiz sexta na forma inconveniente:
return 64 >>= (\x -> sqrt' x) >>= (\y -> cbrt' y)
Agora podemos reescrever isso da forma legível
do let x = 64 y <- sqrt' x z <- cbrt' y return z
Nós podemos escrever o que parece um código ordinário não multi-valorado e as
funções bind
implícitas que Haskell insere automaticamente o tornam
multi-valorado.
Inventar esta sintaxe foi um trabalho de gênio. Talvez você pudesse tê-la inventado, mas eu tenho certeza que eu não teria. Mas este material extra é mero açúcar sintático em cima das mônadas. Eu ainda afirmo que você poderia ter inventado as mônadas subjacentes.
7 Entrada&Saída
Existe agora uma última coisa a ser observada antes de você estar completamente
qualificado em monadicidade. Interação com o mundo exterior. Até agora tudo o
que eu tenho dito pode aplicar-se a qualquer linguagem funcional pura. Mas agora
considere linguagens funcionais puras preguiçosas. Em uma tal linguagem, você
não tem ideia de que ordem as coisas serão evaluadas. Então se você tem uma
função para imprimir a mensagem "Insira um número" e outra para inserir o
número, você pode não ser capaz de garantir que a mensagem será impressa antes
da entrada ser requerida! Volte ao exemplo da função randomizada. Note como a
semente randomizada acaba costurada ao longo das suas funções de modo que ela
pode ser usada cada vez que random
é chamada, Existe um tipo de ordenamento
acontecendo. Suponha que tenhamos x >>
f >>= g=. Como g
usa a
semente retornada por f
, sabemos com certeza que f
gerará seu número
aleatório antes de g
. Isto mostra que, em princípio, mônadas podem ser usadas
para ordenar computações.
Agora considere a implementação de random
no compilador. Ela é tipicamente uma
rotina C ou ASM ligada no executável Haskell final. Se esta rotina fosse
modificada de forma a realizar E/S poderíamos garantir que a E/S em f
foi
realizada antes daquela em g
. Isto é exatamente como E/S funciona em Haskell,
nós realizamos toda a E/S numa mônada. Neste caso, uma função que
conceitualmente é do tipo a -> b
mas também tem um efeito colateral
no mundo real é na realidade do tipo a -> IO b
. O tipo IO é uma
caixa-preta, não precisamos saber como ela é. (Talvez ema funcione como o
exemplo random
, talvez não.) Nós apenas precisamos saber que x >>
f >>= g= executa a E/S em f
antes de g
.
8 Teoria das Categorias
Uma última coisa. Mônadas são originalmente desenvolvidas no contexto da teoria das categorias. Vou deixar esta conexão para outro dia.
Oh!… e eu devo mencionar… ainda não estou convencido de que eu poderia inventar Sequências Espectrais. Mas ainda estou trabalhando nisso graças a Tim Chow.
9 Apêndice: Um Exemplo de Código Completo Usando Números Aleatórios
Primeiro, eis o código correspondente às definições acima:
import Random bind :: (a -> StdGen -> (b,StdGen)) -> (StdGen -> (a,StdGen)) -> (StdGen -> (b,StdGen)) bind f x seed = let (x',seed') = x seed in f x' seed' unit x g = (x,g) lift f = unit . f
Então eis o que faremos: queremos construir um número aleatório de dois dígitos em três passos. Começando de 0, nós vamos:
- adicionar um inteiro aleatório de 0 a 9
- multiplicá-lo por 10
- adicionar outro inteiro aleatório de 0 a 9
Conceitualmente esta operação é uma composição similar a addDigit
. (*10) . addDigit
. Mas nós sabemos que precisamos costurar a semente aleatória
ao longo desse código. Primeiro, repare a real definição de addDigit
:
addDigit n g = let (a,g') = random g in (n + a `mod` 10,g')
Isto retorna um par consistindo de n+digitoAleatorio
e a nova semente. Note
como isto também recebe a nova semente como argumento. Pode-se dizer que
'conceitualmente' esta função é addDigit n = let a = random in n + a
`mod` 10
e numa linguagem estrita e impura adequada isto seria considerado um
código válido.
Agora considere a operação de multiplicar por 10. Esta é uma operação ordinária
que podemos 'melhorar' usando lift
. Então obtemos
shift = lift (*10)
Agora o ponto crucial. Não podemos simplesmente compor, mas em vez disso devemos
usar bind
para converter nossas funções em uma forma que podem ser
compostas. Então o conceitual addDigit . (*10) . addDigit
torna-se
test :: Integer -> StdGen -> (Integer,StdGen) test = bind addDigit . bind shift . addDigit
Agora criamos a semente usando meu número favorito e rodamos o código:
g = mkStdGen 666 main = print $ test 0 g
Note que eu decidi não usar a classe de tipos Monad
nativa de forma que nada
ficasse oculto e que tudo estivesse explícito.
Atualização: Agradeço pelos comentários! Eu fiz uma série de consertos e adicionei um apêndice com um exemplo de números aleatórios. A maioria dos fragmentos de código agora foi copiada para o texto final em código real, então deve haver apenas alguns erros de digitação. Desculpas por qualquer confusão causada!
Para uma abordagem diferente sobre mônadas, eu escrevi um, pouco sobre as flechas de Kleisi. E se você estiver interessado em empilhar mônadas para obter mais funcionalidades, você pode tentar isso.
10 META
Autor | Dan Piponi |
Título Original | You Could Have Invented Monads! (And Maybe You Already Have.) |
Link Original | http://blog.sigfpe.com/2006/08/you-could-have-invented-monads-and.html |
Link Arquivado | http://archive.is/Jox0z |
Footnotes:
Existe até uma página na Haskell Wiki dedicada a isso!
Não conheço termo melhor para plumbing - "encanamento", no sentido de "serviço de encanador/pedreiro"…
Created: 2018-08-28 ter 09:42
Nenhum comentário:
Postar um comentário