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:
main = putStrLn "Hello World"
data Something = Something String
E expressões, como:
putStrLn "Hello World"
10 + 20
Something "Here"
if this then that else otherThat
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:
$(funcaoQueRetornaCodigo)
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:
#define SQU(x) x*x
Aqui definimos um macro SQU
que deve ser expandido para x*x
. Dessa forma,
se escrevermos:
SQU(2)
O preprocessador vai expandir isso para:
2*2
De fato, se você compilar e executar:
#include<stdio.h>
#define SQU(x) x*x
int main() {
printf("Answer: %d", SQU(3));
return 0;
}
Verá:
Answer: 9
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:
#include<stdio.h>
#define SQU(x) x*x
int main() {
printf("Answer: %d", SQU(2)/SQU(2));
return 0;
}
Teremos o inesperado resultado:
Answer: 4
Isso é porque o macro acima é expandido para:
#include<stdio.h>
#define SQU(x) x*x
int main() {
printf("Answer: %d", 2*2/2*2);
return 0;
}
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:
#define SQU(x) ((x)*(x))
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:
(funcao argumento1 argumento2 argumento3)
Assim, somamos dois números com:
(+ 10 20)
E mesmo as estruturas de controle de fluxos são expressas com listas:
(if condicao "caso true!" "caso false!")
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:
(defmacro infix [infixed]
(list (second infixed) (first infixed) (last infixed)))
Acima construímos um macro que permite o uso infixo de funções:
(infix (10 + 20))
Simplesmente, ele se expande para a chamada do nosso segundo exemplo:
(macroexpand '(infix (10 + 20)))
; => (+ 10 20)
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 :: Q [Dec]
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:
ghci> :set -XTemplateHaskell
Imediatamente podemos ver o tipo de uma QuasiQuotation:
ghci> :t [| 10 + 20 |]
[| 10 + 20 |] :: Language.Haskell.TH.ExpQ
Vamos importar o módulo Language.Haskell.TH
para entender o que é ExpQ
:
ghci> import Language.Haskell.TH
ghci> :info ExpQ
type ExpQ = Q Exp -- Defined in ‘Language.Haskell.TH.Lib’
Para inspecionar o Exp
contido nesse Q
, vamos usar a função runQ
, cuja
assinatura pode ser simplificada para:
runQ :: IO => Q a -> IO a
Então:
ghci> runQ ([e| 10 + 20 |])
InfixE (Just (LitE (IntegerL 10))) (VarE GHC.Num.+) (Just (LitE (IntegerL 20)))
ghci> runQ ([d| data List = List String |])
[DataD [] List_0 [] [NormalC List_1 [(NotStrict,ConT GHC.Base.String)]] []]
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:
class ShowType a where
showType :: a -> String
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:
data MeuTipo = MeuTipo
instance ShowType MeuTipo where
showType = "MeuTipo"
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.
ghci> :t 'MeuTipo
'MeuTipo :: Name
Da mesma forma também adiciona a sintaxe ''Coisa
que transforma um tipo
em seu Name
.
ghci> :t ''MeuTipo
'MeuTipo :: Name
'MeuTipo
é o Name
do construtor MeuTipo
e ''MeuTipo
é o Name
do tipo
MeuTipo
.
Se escrevermos o que temos até agora no ghci
:
ghci> class ShowType a where showType :: a -> String
ghci> data MeuTipo = MeuTipo
Podemos dar uma olhada na estrutura para a instância que gostaríamos de gerar:
ghci> runQ [d| instance ShowType MeuTipo where showType _ = "MeuTipo" |]
Com highlighting para ser mais fácil de enxergar:
[ InstanceD []
(AppT (ConT ShowType) (ConT MeuTipo))
[ FunD showType [ Clause [ WildP ]
-- Nosso gerador só precisa mudar essa parte
(NormalB (LitE (StringL "MeuTipo")))
[]
]
]
]
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:
deriveShowType :: Name -> Q [Dec]
deriveShowType name = return [ InstanceD []
(AppT (ConT ''MyShow) (ConT name))
[ FunD 'myShow [ Clause [ WildP ]
(NormalB (LitE
(StringL
(nameBase name))))
[]
]
]
]
Usando o gerador
Podemos usar o gerador por meio dos splices:
{-# LANGUAGE TemplateHaskell #-}
module Main where
-- O Template Haskell não nos deixa usar splices com funções definidas no
-- módulo atual então botamos tudo escrito até agora em um módulo `TH`
import TH
data MyType = MyType
deriveShowType ''MyType
main :: IO ()
main = print (myShow MyType)
Refatorando com pattern-matching
Com pattern matching, poderíamos usar os próprios QuasiQuotes
para a ajudar a
gerar o código:
deriveShowType name = do
-- Criamos a instância que queremos
ids <- [d| instance MyShow String where myShow _ = "Mock" |]
-- Desconstruímos a instância
let [InstanceD ctx (AppT showt (ConT _)) _] = ids
name' = nameBase name
-- Aqui há uma coisa importante, podemos acessar as variáveis em escopo
-- dentro do QuasiQuoter e elas serão interpoladas.
fds <- [d| myShow _ = name' |]
-- Reconstruímos a instância substituindo o "Mock"
return [InstanceD ctx (AppT showt (ConT name)) fds]
QuasiQuotes Take 2: O que é um QuasiQuoter?
Um QuasiQuoter
não passa de um ADT definido como:
data QuasiQuoter = QuasiQuoter { quoteExp :: String -> Q Exp,
quotePat :: String -> Q Pat,
quoteType :: String -> Q Type,
quoteDec :: String -> Q [Dec] }
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.