Meta-programação em Haskell - Parte 1 - C, LISP, Template Haskell e QuasiQuotes
por Pedro Tacla Yamada
Algumas linguagens mudam do dia para noite, quebrando uma quantidade enorme de código que estava em produção. Outras demoram anos para avançar. Há uma formas melhores da comunidade introduzir mudanças organicamente; um sistema de macros sendo uma forma popular. Gostaria de discutir o quê o Haskell traz para essa frente, propostas que para mim são novas e inusitadas.
Em um segundo post, tentarei mostrar um exemplo de meta-programação que soluciona um problema sério na linguagem e como sua criação impulsionou mudanças no compilador.
Não tenho como recomendar o suficiente o paper/talk de Guy Steele “Growing a Language”, no qual discute como a extensibilidade por meio da composição de primitivas básicas é um conceito fundamental em linguagens e no quão bem sucedidas serão. Não se refere somente à linguagens de programação, ainda que esse seja seu propósito prático.
Se nunca leu esse texto, adicione ele para sua lista de leituras! Eu o guardo com muito carinho porque foi o primeiro “Paper of the Week” enquanto estava na Hacker School/Recurse Center e é uma leitura sensacional e muito acessível.
Em “Growing a Language”, Guy Steele discute a ideia de que “linguagem” não se trata somente de prover formas de expressão, mas também de prover possibilitar a criação de novas formas de expressão. Uma boa linguagem deve ser capaz de ser extendida, estruturas devem ser capazes de serem compostos de forma a criar novos estruturas, novos sentidos e novas formas de composição.
Template Haskell
Começarei com uma discussão sobre o Template Haskell. No dia 9 da série 24 dias de Hackage, Franklin Chen comentou levemente sobre o Template Haskell e como pode melhorar seu código. E o que é Template Haskell?
Como linkado no dia 9, há um dia de hackage de 2014 sobre Template Haskell por Oliver Charles. Farei meus próprios comentários sobre a extensão.
{-# LANGUAGE TemplateHaskell #-}
Entre outros elementos da sintaxe, podemos dizer que a linguagem basicamente se separa em:
Declarações, como:
E expressões, como:
Também temos patterns e tipos, mas como ambos sempre fazem parte de declarações ou expressões, acho que podemos dizer que (basicamente) isso de fato é tudo.
Ao adicionar {-# LANGUAGE TemplateHaskell #-}
ao topo de um arquivo .hs
,
ativamos a extensão. Adiciona a ideia de “splices” ao Haskell. Dada uma
funcaoQueRetornaCodigo
que retorna código e cabe em algum lugar, podemos
escrever:
E o código será gerado a tempo de compilação e incluso nesse local.
O que é “código”?
Antes de tratar do que é “código” no Template Haskell, vamos olhar brevemente para os sistemas de macros em C e em seguida em LISP (usando o Clojure como exemplo).
Nota: Não tenho experiência real em C ou Clojure, se tiver algo a dizer, não hesite em comentar! Há uma seção de comentários no fim do post.
Macros em C, search-and-replace
Em C, o sistema de macros é efetivamente um “template”, no mesmo sentido de um template de HTML, como o que contém o layout desse post. Uma instância de um macro em C é estupidamente inserida no código, sem garantia de que o que retorna tem o mesmo sentido ao ser compilado que o autor do macro esperava que tivesse.
Um exemplo tirado do site c4learn.com:
Aqui definimos um macro SQU
que deve ser expandido para x*x
. Dessa forma,
se escrevermos:
O preprocessador vai expandir isso para:
De fato, se você compilar e executar:
Verá:
Claro que escolhi esse exemplo porque ele mostra o que quero dizer com estupidamente. Aqui não estamos protegendo as expressões com parênteses, então, se escrevermos:
Teremos o inesperado resultado:
Isso é porque o macro acima é expandido para:
E 2*2/2*2
é executado como:
2*2/2*2
- Reduz2*2
4/2*2
- Reduz4/2
2*2
- Reduz2*2
4
Outros problemas vão aparecer se tentarmos escrever SQU(2 + 2)
e assim por
diante, a solução sendo usar parênteses em volta do macro:
Macros em Clojure, código como dados
Não poderia discutir de meta-programação sem passar, mesmo que superficialmente, pelo LISP. Em LISP, temos a grande vantagem de que a sintaxe é quase exclusivamente composta por listas. Para aplicar uma função, escrevemos:
Assim, somamos dois números com:
E mesmo as estruturas de controle de fluxos são expressas com listas:
Isso é muito útil, porque podemos facilmente escrever código que manipula código. Um macro é simplesmente uma função especial que retorna a representação do código que deve ser expandida para. Um exemplo do site braveclojure.com:
Acima construímos um macro que permite o uso infixo de funções:
Simplesmente, ele se expande para a chamada do nosso segundo exemplo:
Temos uma diferença radical do que há em C. Ao invés de lidarmos com texto que será inserido no código, lidamos com a AST: a árvore de sintaxe abstrata. Isso é, temos uma estrutura de dados que representa a sintaxe da linguagem. No caso do LISP, manipular essa estrutura de dados é facilitado pelo fato da sintaxe ser extremamente simples e da tradução entre estrutura de dados e código ser direta.
Template Haskell, manipulação tipada do código
Haskell segue na mesma linha do LISP. Trabalhamos sobre a AST tentando retornar e manipular estruturas de dados que representem o código, mas ao contrário do LISP que é dinâmico e tem a vantangem de traduzir a estrutura de dados para o código diretamente, no Haskell, a árvore é tipada.
Como tratamos acima, a linguagem basicamente é composta por duas estruturas de dados:
Também temos
literais representados por Lit
e
patterns representados por Pat
.
Ao digitar $(funcaoQueRetornaCodigo)
, o tipo esperado de
funcaoQueRetornaCodigo
depende do contexto. No top-level, esperamos que tenha
tipo:
funcaoQueRetornaCodigo
pode usar qualquer parte ou módulo já escrito em
Haskell, desde que não use código definido no mesmo módulo em que está sendo
incluso. Isso quer dizer que, se temos código que faz parsing de Markdown para
uma estrutura de dados em Haskell e somos capazes de fazer dessa estrutura uma
declaração, podemos escrever $(parseMarkdown "markdown")
e esperar que a
estrutura esteja definida nesse ponto.
O que é Q?
retornaCodigo
tem tipo Q algumaCoisa
, onde algumaCoisa
é uma das
estruturas de dados para código (Pat
, Exp
, [Dec]
etc.). Q
é um Monad
que nos deixa facilmente:
- Gerar nomes únicos que não conflitem com nomes já definidos
- Encontrar os identificadores para a estrutura a qual strings se referem
- Manejar estado
- Fazer
IO
(podemos ler um arquivo ou baixar documentação da web para gerar código; isso é inseguro, porque pode fazer a compilação não ser determinística, mas é legal que seja possível e tem muitos usos práticos)
QuasiQuotes Take 1: Explorando a AST do Haskell
Há uma última coisa que o Template Haskell adiciona à linguagem. são as
QuasiQuotes. Adiciona a sintaxe [quoter| conteúdo |]
onde quoter
recebe
uma String
e retorna um Q
de alguma das estruturas de dados para o
código. Vamos entrar mais profundamente nisso, mas agora, sugiro que você abra
uma sessão do GHCi e brinque com os QuasiQuoters que são importados por
padrão ao se ativar a extensão.
Primeiro inicializamos o ghci
e ativamos a extensão:
Imediatamente podemos ver o tipo de uma QuasiQuotation:
Vamos importar o módulo Language.Haskell.TH
para entender o que é ExpQ
:
Para inspecionar o Exp
contido nesse Q
, vamos usar a função runQ
, cuja
assinatura pode ser simplificada para:
Então:
Temos os QuasiQuoters
built-in:
e
para expressõesd
para declaraçõesp
para patternst
para tipos
Quando chamamos [| 10 + 20 |]
o quasi quoter, em tempo de compilação:
- Recebe
10 + 20
como umaString
- Parseia
"10 + 20"
como uma expressão de Haskell - Retorna um
Exp
Estou mostrando isso porque ainda que no Template Haskell nós não tenhamos a vantagem que se tem no LISP de uma árvore de sintaxe abstrata extremamente simples e direta, temos essa capacidade de interativamente inspecionar estruturas.
Derivando Show com Template Haskell
Um exemplo prático de Template Haskell.
Queremos gerar instâncias de uma type-class ShowType
ilustrativa, que é
definida como:
Dado um valor x
de tipo a
, esperamos que showType x
imprima a
como uma
String
.
Uma implementação manual para um ADT mínimo seria:
Para gerar o código queremos gerar uma declaração da instância ShowType
para
algum tipo arbitrário. Queremos portanto uma função que receba o nome do tipo,
como um Name
e retorne um Q [Dec]
contendo a declaração. Name
é um tipo
especial para identificadores; é o tipo que o Monad
Q
nos retorna quando
pedimos um nome único ou tentamos encontrar o identificador que uma String
se
refere para. Poderíamos fazer o mesmo usando String
s, mas iriamos perder um
pouco da tipagem.
O TemplateHaskell
adiciona a sintaxe 'coisa
que transforma algo em seu
Name
. Afinal, normalmente na linguagem, se tiver um ADT MeuTipo
, MeuTipo
em uma expressão se referiria ao construtor, não ao nome do construtor.
Da mesma forma também adiciona a sintaxe ''Coisa
que transforma um tipo
em seu Name
.
'MeuTipo
é o Name
do construtor MeuTipo
e ''MeuTipo
é o Name
do tipo
MeuTipo
.
Se escrevermos o que temos até agora no ghci
:
Podemos dar uma olhada na estrutura para a instância que gostaríamos de gerar:
Com highlighting para ser mais fácil de enxergar:
Vamos usar a função nameBase
do módulo Language.Haskell.TH.Syntax
, que
define as estruturas que representam código linkadas acima. Essa função tem
tipo Name -> String
.
Agora é só escrever:
Usando o gerador
Podemos usar o gerador por meio dos splices:
Refatorando com pattern-matching
Com pattern matching, poderíamos usar os próprios QuasiQuotes
para a ajudar a
gerar o código:
QuasiQuotes Take 2: O que é um QuasiQuoter?
Um QuasiQuoter
não passa de um ADT definido como:
Assim para ter uma linguagem interpolada no Haskell basta definir essa estrutura. Sugiro dar uma olhada no manual do GHC. Um dos projetos da HaskellBR está usando um QuasiQuoter para ler a declaração de um ADT a partir de arquivos markdown e um header YAML-frontmatter
(Ele usa o meu módulo frontmatter
)
O que vimos e todo o código
Esse post tentou introduzir a meta-programação em Haskell. Em suma, gostaria de ter apresentado que:
- Em Haskell a meta-programação em tempo de compilação se dá por meio da manipulação da AST
- Temos segurança de tipos ao gerar código
- Podemos explorar a AST usando QuasiQuoters
- Podemos definir novas estruturas de sintaxe usando QuasiQuoters
Todo o código está disponível no novo repositório haskellbr/blog-code
no GitHub
No próximo post, vou me aprofundar nesse último ponto e como é um diferencial para a comunidade.