Home

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

Vamos nos encontrar em São Paulo em 25 de Janeiro de 2016. Marque sua presença e comente se não puder vir.

Í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.

Há um milestone no GitHub com tarefas esperando por você..

Share Comente no Twitter