Home

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:

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:

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:

Quando chamamos [| 10 + 20 |] o quasi quoter, em tempo de compilação:

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 Strings, 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:

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.

Share Comente no Twitter