24 dias de Hackage, 2015 - dia 17 - ansi-wl-pprint: Evitando hackear com strings
por Franklin Chen
traduzido por Pedro Yamada
Esse é um artigo escrito por Franklin Chen e traduzido para o português. Ler original.
Encontro HaskellBR São Paulo
Índice de toda a série
O índice de toda a série está no topo do artigo para o dia 1.
Dia 17
Hoje, fazemos o inverso do que fizemos no day 14, que foi parsear de texto para uma árvore de sintaxe abstrata. Hoje nós fazemos o pretty-printing de uma árvore abstrata para texto.
Eu acho que no mundo mais amplo da programação, é muito comum ver soluções inflexíveis e ad-hoc para esse problema, usando string hacking e talvez, no melhor dos casos, templates (baseados em strings). Tipicamente, é difícil customizar estilos de indentação rápidamente, amarrar expressões e outras funcionalidades como uma solução ad-hoc.
Na comunidade de Haskell, uma solução melhor é mais comum, por causa do número de bibliotecas de qualidade para ajudar com pretty-printing.
ansi-wl-pprint
é uma dessas bibliotecas, que incluí não só muitos combinadores úteis, mas
também expõe suporte para output em cores, se você precisa disso (você não
precisa).
Update
Um comentário no Reddit notou que há outra versão da biblioteca de
pretty-printing,
annotated-wl-pprint
.
Isso parece ser muito legal, e há um
video por David Christiansen sobre seu uso com Idris.
De volta a nosso tipo de expressão de exemplo
Lembre que nosso tipo de expressão é:
-- | Variable in an expression.
type Var = String
-- | Expression.
data Exp
= N Int -- ^ number
| V Var -- ^ variable
| Plus Exp Exp -- ^ sum
| Times Exp Exp -- ^ product
Alguns exemplos de output pretty-printed
Vamos fazer pretty-print em uma expressão de exemplo. (Na vida real, nós criaríamos um corpo de expressões para pretty-print e as verificaríamos, e também escreveríamos testes do QuickCheck para confirmar várias propriedades.)
{-# LANGUAGE QuasiQuotes #-}
module SymbolicDifferentiation.AnsiWlPprintSpec where
import qualified SymbolicDifferentiation.Earley as Earley
import qualified SymbolicDifferentiation.AnsiWlPprint as AnsiWlPprint
import Test.Hspec (Spec, hspec, describe, it, shouldBe, shouldSatisfy)
import Data.String.Here (hereLit)
spec :: Spec
spec =
describe "anti-wl-pprint for symbolic differentiation expression" $ do
let eString = [hereLit|(x*1) + 2*y + ((3+z) * (4*b)) * (5+d+w)|]
let ([e], _) = Earley.parses eString
it eString $ do
show (AnsiWlPprint.prettyPrint e) `shouldBe`
[hereLit|x*1 + 2*y + (3 + z)*4*b*(5 + d
+ w)|]
Para evitar o problema de construir um valor Exp
na mão, estamos reusando
nosso parser.
Um exercício para o leitor: escreva um gerador do QuickCheck de expressões
randômicas, e escreva um teste que verifica que quando você passa uma expressão
aleatória Exp
pelo pretty-printer e de volta pelo parser para uma Exp
,
você recebe o mesmo resultado!
O exemplo aqui formada somas separadas por espaços e produtos concatenados juntos, com parênteses mínimos e quebras de linha opcionais.
O código
O código é curto. A
documentação Haddock para o ansi-wl-pprint
é excelente, então você pode procurar nela para uma explicação dos combinadores
usados.
O conceito fundamental por trás da biblioteca é que há um tipo de dados Doc
que representa um “documento”, uma peça de informação pretty-printed, e que
há uma algebra combinando os documentos para os organizar de várias formas,
então você pode usar uma variedade de interpretadores sobre o documento para
realizar a conversão para string no final.
module SymbolicDifferentiation.AnsiWlPprint where
import SymbolicDifferentiation.AlphaSyntax (Exp(N, V, Plus, Times))
import Text.PrettyPrint.ANSI.Leijen
( Doc
, int, text, char
, (</>), (<//>), (<+>), (<>)
, parens
)
-- | Very primitive, for illustration only!
--
-- Spaces for sums, but no spaces for products.
-- Soft breaks before operators.
prettyPrint :: Exp -> Doc
prettyPrint e = p 10 e
-- | Pretty-print inside a precedence context to avoid parentheses.
-- Consider + to be 6, * to be 7.
p :: Int -> Exp -> Doc
p _ (N n) = int n
p _ (V v) = text v
p prec (Plus e1 e2) = maybeParens (prec < 7)
(p 7 e1 </> char '+' <+> p 7 e2)
p prec (Times e1 e2) = maybeParens (prec < 6)
(p 6 e1 <//> char '*' <> p 6 e2)
maybeParens :: Bool -> Doc -> Doc
maybeParens True = parens
maybeParens False = id
Exercício para o leitor: Há muitas formas de melhorar o pretty-printer, por exemplo alinhar os termos para ficar como:
a + b*c
+ d*e
+ f*g
Eu provavelmente primeiro transformaria uma Exp
em uma SugaredExp
que
representa a sintaxe abstrata intermediária dos termos e fatores, e então
escrever um pretty-printer sobre isso.
Duplicação de trabalho?
Você deve se perguntar sobre a duplicação de trabalho em escrever o parser e o pretty-printer. Não podemos escrever uma única especificação high-level para fazer ambos simultâneamente? Sim, há ferramentas para isso, mas estão fora do escopo desse artigo.
Conclusão
Pretty-printing é uma parte importante de uma interface baseada em texto,
seja para inspecionar código gerado ou criar boas mensagens de
erro. ansi-wl-pprint
é uma boa biblioteca para usar para pretty-printing>
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.