24 dias de Hackage, 2015 - dia 10 - s-cargot: Usando a sintaxe de s-expressions
por Franklin Chen
traduzido por Pedro Yamada
Esse é um artigo escrito por Franklin Chen e traduzido para o português. Ler original.
Índice de toda a série
O índice de toda a série está no topo do artigo para o dia 1.
Dia 10
Há momentos em que eu tenho inveja do mundo do Lisp.
Um desses momentos é quando estou definindo alguma linguagem de domínio específico, porque no mundo do Lisp, a escolha natural é a representar usando s-expressions como a sintaxe concreta, e não se preocupar com definir ainda outra sintaxe especial, junto com escrever um parser para essa sintaxe para a sintaxe abstrata e também um pretty-printer da sintaxe abstrata para a sintaxe concreta. Talvez no longo termo, os usuários podem querer sintaxe especial que não é só s-expressions, mas para protótipos iniciais, pelo menos, parece valoroso não se comprometer com nenhuma sintaxe e só usar s-expressions. Apesar de que não há nada de mágico sobre s-expressions (XML, JSON ou qualquer outra representação genérica de uma estrutura de árvore), elas são particularmente concisas e flexíveis.
S-expressions são tão úteis que no meu primeiro emprego como um engenheiro de software nos anos 90, nós tínhamos uma biblioteca de s-expressions para C++ para “inputar” e “outputar” um formato que era basicamente uma linguagem de domínio específico que era processada por uma gama de ferramentas (isso era antes do XML ser inventado e incluía um validador que eu escrevi em Standard ML).
A biblioteca no Hackage para trabalhar com s-expressions em Haskell é a
s-cargot
.
Muitas outras já existiram, mas quase todas foram aos poucos deixando de ser
mantidas, enquanto essa é nova e vem com todo tipo de funcionalidade.
Hoje, darei um exemplo de como usar essa biblioteca, no contexto de um domínio de problemas no qual ter uma sintaxe concreta é importante.
Instalação
Precisamos adicionar s-cargot
para o stack.yaml
:
A tarefa: matemática simbólica
A tarefa a ser resolvida aqui é, aproximadamente o suficiente, uma tradução de código Lisp (Scheme, para ser preciso) para um diferenciador simbólico de expressões matemáticas no livro clássico de ciência da computação “Structure and Interpretation of Computer Programs” (SICP). Não vou descontruir a solução aqui, mas só me concentrar em alguns problemas de sintaxe.
Um exemplo: dada uma expressão matemática como a função linear
5x + 7
, nós queremos encontrar a derivativa simbólida em respeito a x
, para
obter 5
.
A sintaxe mais simples para o tipo da expressão
Como modelamos uma expressão e uma função para computar a derivativa de uma
expressão? Vamos começar com a forma mais crua possível, que é definir um tipo
de dados para a expressão, Exp
, junto com os construtores alfanuméricos N
(para números), V
(para variáveis), Plus
(para a soma de sub-expressões) e
Times
(para o produto de sub-expressões). Um pedaço da suite do
HSpec/QuickCheck que define o que precisamos:
A sintaxe parece OK para expressões simples, mas horrível quando há muitas sub-expressões dentro de outras, com parênteses para o agrupamento, como no último exemplo artificial.
Aqui está o código para um ainda ingênuo diferenciador simbólico, tendo apenas
algumas heurísticas embutidas para um pouco de simplificação por meio de
reescrita (por exemplo, adicionar 0
a uma expressão resulta na própria
expressão ao invés de construir uma sub-expressão N 0
superflua):
A sintaxe do código, usando pattern matching, parece razoavelmente boa para
mim. É a construção de um Exp
que é um pouco feia, mas não terrível. Então
estamos em uma situação na qual o implementador dessa linguagem está feliz, mas
o usuário não. Na verdade, nós nem expusemos uma forma para um usuário fora do
sistema criar expressões: até agora, temos uma API mas nenhum parser de string
para expressão.
Parsing a partir de qual formato?
Uma forma de prosseguirmos seria escrever um parser para uma sintaxe
customizada, por exemplo, poderíamos escrever uma função fromString :: String
Exp
tal que:
Nós também teríamos que escrever um pretty-printer toString :: Exp ->
String
inteligente o suficiente para voltar para a forma original:
É uma tarefa direta escrever um parser desses usando o
parsec
ou algo do gênero, mas você pode imaginar que as vezes é um pouco irritante
desenhar ou fazer os usuários aprenderem a sintaxe de uma linguagem muito mais
complexa (incluindo operadores infixos, precedência, escopo, tipos de blocos
diferentes, etc.).
Então vamos assumir para o propósito desse artigo que nós temos uma razão para preferir s-expressions com a “notação polonesa reversa”, somente composta de prefixos, da mesma forma que a comunidade de Lisp faz para evitar todas as desvantagens de uma sintaxe especial.
(Update 14/12/2015) Usando Earley
para expor uma sintaxe customizada
No
dia 14,
criei uma sintaxe customizada e um parser usando a biblioteca de parsing Earley
.
Testes do QuickCheck
Aqui estão alguns testes de exemplo do QuickCheck para mostrar o que queremos
ser capazes de fazer. (Note que por conveniencia, estamos usando interpolação de strings com o pacote
here
como introduzido ontem,
dia 9.)
Incluí um pequeno teste para indicar que nós também queremos alguma forma de capturar e lidar com erros. Nada é mais irritante para um usuário do que mensagens de erros de parse ruins. Não podemos oferecer mensagens ótimas aqui, mas pelo menos vamos dar uma ideia de como poderíamos o fazer.
O parser de s-expressions
s-cargot
expõe formas muito flexíveis de construir parsers de
s-expressions, baseadas em que tipo de sintaxe você quer permitir (Scheme
completo ou não, por exemplo) e permite hooks em muitos níveis para suportar
“leitores” customizados e também especificar o parser de átomos desejado.
Antes de mais nada, alguns imports:
A parte principal é uma pipeline que chama um parser s-cargot
e então chama o
nosso próprio parser de uma estrutura representando uma s-expression para
nosso tipo Exp
:
Uma vez tendo uma s-expression bem formada, podemos a desestruturar, encontrando os possíveis erros.
Pretty-printing
E nós ganhamos pretty-printing de graça do s-cargot
se nós transformarmos
nosso Exp
em uma s-expression. Não vou mostrar detalhes, mas você pode usar
o printer básico de s-expressions ou o customizar com muitas opções
incluíndo a estratégia de indentação. Vamos usar o básico.
Um exemplo de um teste para nosso SExp.prettyPrint
:
O código:
Resumo de parsing e pretty-printing de s-expressions
Aí está: com um pouco de boilerplat, você pode ter uma experiência similar à de trabalhar com Lisp. Note que com um pouco de trabalho de Template Haskell usando “quasiquotes”, você poderia ir mais longe dos templates de texto que criamos e tabém criar templates de “patterns”
Não tivemos que escrever um parser tradicional e fomos capazes de separar o que era be formado do que precisava ser processado. Isso é muito útil em muitos contextos: na minha experiência, a checagem de erros em vários níveis faz mensagens de erros boas mais fáceis de serem implementadas. Além disso, algo não discutido aqui é como s-expressions podem ajudar com predicção e auto-completion.
Mais notas opcionais sobre a sintaxe para DSLs
Eu queria apontar que para muitos domínios de problemas, como esse que acontece de ser matemático, as vezes é popular fazer as coisas parecerem matemáticas. Eu apresentei tudo sem essa intenção primeiro, mas agora vou mostrar algumas variações de sintaxe.
Identificadores alfanuméricos como operadores
Primeiro, podemos usar a sintaxe de operadores com backticks e níveis de precedência:
Perceba que nada mudou de fato, exceto o uso e definição de funções infixas e o uso de precedência para remover parenteses, como na grande expressão para a derivativa de um produto de sub-expressões. Mas a clareza está começando a ser perdida, para aqueles não familiares com o domínio do problema e suas convenções.
Identificadores simbólicos como operadores
Pode-se ir mais longe e usar identificadores simbólicos ao invés dos operadores alfanuméricos com backticks. Isso é onde muitos de nós começamos a nos perder no que está acontecendo:
Dependendo do seu gosto pessoal, pode achar que essa sintaxe engraçadinha faz os testes ficarem mais bonitos:
Tudo isso é um pouco fofo se você está acostumado, mas eu acho que pode ser muito estranho caso contrário; e eu sei que muitos não-Haskellers vendo isso antes de ver a versão crua tem a ideia errada de que você precisa escrever as coisas dessa forma em Haskell e viram as costas em desgosto e confusão. Isso é porque eu mostrei a forma crua antes, e estou mostrando essa só para ilustrar que há bibliotecas que vão tentar ser bem sugestivas no uso de operadores, e também que não há nada de especial acontecendo: é só uma sintaxe diferente para expressar exatamente a mesma coisa que na primeira versão, com a segunda versão (operadores alfanuméricos com backticks) sendo um passo de transição em direção a essa terceira versão. É bom saber as três variações, independente de qual você prefere ler ou escrever.
Mas e a ideia de usar s-expressions, que era desacoplar o lado do usuário (representado por testes) da sintaxe abstrata e do Haskell? Minha intuição é que há benefícios em ter uma sintaxe alternativa usando s-expressions para qualquer outra sintaxe concreta disponível.
Conclusão
S-expressions são uma forma testada pelo tempo de representar dados. A
biblioteca s-cargot
vem com muitas formas de criar parsers de s-expressions
customizados e pretty-printers e também vem com padrões úteis.
Todo o código
Todo o código para a série estará nesse repositório do GitHub.
Nota do tradutor
Se você quer ajudar com esse tipo de coisa, agora é a hora. Entre no
Slack ou no
IRC da HaskellBR e
contribua. Esse blog e outros projetos associados estão na
organização haskellbr
no GitHub e em
haskellbr.com/git.