24 dias de Hackage, 2015 - dia 2 - Expressões regulares com pcre-heavy; scripts standalone usando Stack
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 2
Não ria, mas muito tempo atrás, minha linguagem de programação principal era Perl (entre meados de 1999 e 2010). Havia uma série de razões para isso; uma delas era que Perl fazia processamento de texto usando expressões regulares ser muito fácil.
Se você é um Haskeller experiente, você pode estar pensando “Por que não usar um parser de verdade?”, algo usando o venerável parsec, coberto em um dia de Hackage de 2012.
(Hoje, várias outras bibliotecas alternativas para parsing poderiam ser consideradas. Outro post dirá mais sobre isso!)
Afinal, como Jamie Zawinski escreveu:
Algumas pessoas, quando confrontadas com um problema, pensam: “Já sei, vou usar expressões regulares”. Agora elas tem dois problemas.
Eu até dei uma palestra no Pittsburgh Tech Fest em 2013, “Stop overusing regular expressions!” [“Pare de abusar de expressões regulares!”], na qual eu promovi escrevermos parsers ao invés de regexes.
Ainda assim, às vezes eu quero usar uma expressão regular. Nesse caso, eu tenho
usado um pacote obscuro porém útil: pcre-heavy
.
Hoje vou mostrar como usar o pcre-heavy
, ao mesmo tempo que mostro como
publicar scripts Haskell standalone de um arquivo que só requerem o Stack.
Por que usar regexes?
Antes de começar com o pcre-heavy
, eu acho que deveria explicar quando uso
expressões regulares.
Na época em que trabalhava com muita extração de texto, limpeza (incluindo correção) e reestruturação de dados bagunçados, expressões regulares pareciam a única escolha real. Eu tinha que não perder nenhuma informação mesmo se ela estivesse escondida atrás de barulho, erros gramaticais ou coisas do tipo. Eu não podia usar alguma técnica estatística aproximada; tinha que iterativamente fazer um monte de trabalho exploratório com alguma prompt interativa para gradualmente limpar os dados. Constructs super-poderosos de regexes em Perl pareciam perfeitos para isso.
Mesmo fora desse tipo de uso, não há como negar que regexes podem ser muito convenientes para tarefas simples [N.T.: até pouco tempo o compilador de CoffeeScript era escrito com RegExps…]. Além disso, porque regexes são tão usadas no nosso mundo da programação em geral, se estivermos migrando algumas regexes prontas de código escrito em outras linguagens para Haskell, é conveniente não ter que as reescrever.
Qual biblioteca de regexes usar com Haskell?!
Um recem-chagado a Haskell pode ficar frustrado com a falta de uma única biblioteca padrão e sintaxe para regexes. Quero dizer… Veja essa página da wiki.
Hoje, estou apresentando
pcre-heavy
, uma biblioteca
de expressões regulares que eu tenho usado quando eu quero expressões regulares
(tento não as querer). Ela é bem nova e nem está mencionada nessa página da
wiki.
Alguns dos meus critérios para escolher uma biblioteca de regex:
- Eu quero regexes estilo-Perl. Isso é o que eu estou acostumado com e elas são meio que um padrão no suporte a regexes em muitas linguagens.
- Boa sintaxe é um plus. Uma das vantagens de usar regexes é o quão conciso é escrever patterns, extrair matches etc. Sem isso, eu já pensaria “Por que não só escrever um parser de verdade? De qualquer forma, só leva algumas linhas em Haskell.”
- Alta-performance
Dados esses critérios, usar uma biblioteca PCRE parece ser a escolha certa. Ok, a wiki lista um monte de bibliotecas baseadas em PCRE.
[N.T.: PCRE significa “Perl Compatible Regular Expressions” ou “expressões regulares ‘estilo’ perl”]
pcre-light
é um bom começo.
Esse pacote depende da instalação da biblioteca em C para PCRE.
Eu sou principalmente um usuário de Mac OS X, então eu tenho o suporte a PCRE
instalado com $ brew install pcre
. Também tenho o pacote funcionando no Linux.
Infelizmente, eu não uso Windows, então seria ótimo se alguém pudesse verificar se o
pcre-light
instala no Windows. Eu ia ficar um pouco triste se eu tiver
escolhido uma biblioteca problemática para usuários Windows. [N.T.: eu não]
Recentemente, o pcre-heavy
foi publicado, um wrapper em volta do pcre-light
que usa Template Haskell,
a extensão do GHC que se resume a “macros para Haskell”, permitindo
meta-programação em tempo de compilação (veja o Dia de Hackage de 2014 sobre Template Haskell).
[N.T.: Template Haskell tem a vantagem sobre um “sistema” de macros porque é
seguro; se o código estiver errado, não compila. Além disso, é relativamente
simples gerar expressões complicadas graças ao suporte a: 1. Gerar a estrutura
de dados que representa um pedaço de código automaticamente baseada nele (isso é
útil porque se opera com a AST), 2. ADTs (Abstract Data Types) para cada
estrutura de código (uma função, um where
, um let
etc.). Vale lembrar que
não é a única forma de metaprogramação em Haskell, podemos listar todos os
campos de um tipo por exemplo e os acessar em runtime usando outras técnicas.]
Eu gostei dele, então o uso.
Um programa de exemplo usando o pcre-heavy
pcre-heavy
tem uma documentação decente na sua página do
Hackage, então eu recomendo ler
ela para todos os detalhes em como o usar. Eu só vou dar um exemplo simples no
contexto de um programa que faz algo.
Especificação e alguns testes
Digamos que temos um arquivo de texto com um formato separado por vírgulas contendo:
- Um header fixo
- Um campo “áudio” ou “vídeo” indicando o tipo da mídia associada
- O path de uma transcrição
- Um comentário opcional dizendo se a mídia não existe ou ainda não está linkada na transcrição
(Eu inventei esse exemplo baseado na especificação de texto estruturado chamada CHAT que inclui uma única linha com esse formato, e.g. essa transcrição do Supremo para “Citizens United v. Federal Election Commission”.)
Exemplos que devem dar match:
Exemplos que não devem dar match:
Escrevendo a expressão regular
Aqui está uma expressão regular pcre-heavy
, usando o
quasiquoter
de Template Haskell
re
que constrói uma
Regex
:
PCRE compilada.
Expressão Regular validada no tempo de compilação do Haskell
Uma vantagem do pcre-heavy
para mim é que porque ele usa
Template Haskell, uma regex errada resulta em um erro de compilação, não um erro
de runtime.
Um exemplo desses erros:
Carregar isso no GHCi imprime:
Usando a expressão regular
Vamos usar
scan
para extrair os matches (se existirem) da nossa regex contra uma string.
scan
retorna uma lista preguiçosa de todos os matches possíveis:
Cada match é um par (String, [String])
, onde o primeiro membro é toda a
string que deu match e o segundo é uma lista de todos os grupos na expressão
regular. Na nossa regex, tínhamos três grupos, então um match só pode resultar
em uma lista com três elementos:
Como só queremos o primeiro match (se existir), nós podemos compor nossa
função com
listToMaybe
de Data.Maybe
,
que tem tipo:
então listToMaybe . scan mediaRegex
tem tipo String -> Maybe (String, [String])
.
[N.T.: listToMaybe
é um head
type-safe, retorna Just primeiroElemento
ou
Nothing
.]
Extraindo os dados úteis
Finalmente, o que nós queríamos fazer de verdade depois de dar match era aplicar um pouco de lógica e botar as coisas em um tipo assim que possível, ao invés de entrar no ramo de programação orientada a strings e a listas cujo tamanho depende do contexto.
[N.T.: Ele quer dizer que nós não queremos operar com uma estrutura tipo
resultado !! 0 -- é a mídia
, resultado !! 1 -- é o tipo
etc. Coisas que não
queremos fazer mesmo em linguagens sem tipos ;)]
Digamos que para a nossa tarefa, só as linhas que não estão missing ou unlinked importam. Podemos definir um tipo de dados e usar pattern matching para sair do mundo dinâmico e entrar no mundo tipado do nosso modelo de dados.
Apresentação como um relatório
Agora que acabamos com o mundo das expressões regulares e temos um modelo de dados estruturado, tudo que falta é completar um programa CLI simples.
Temos toda a informação necessária para imprimir um relatório para cada linha.
Para finalizar, jogamos todo o stdin
na nossa lógica:
N.T. Extendida: Desconstruindo esse exemplo
Todo o programa de Haskell precisa declarar um main
, como no C
. Essa é a
primeira linha:
O main
tem tipo IO ()
- uma ação que roda no contexto de Haskell que pode
fazer input e output, e que retorna o tipo vazio ()
(a.k.a. void
).
Seguimos para pegar todo o stdin
como uma lista preguiçosa usando s <-
getContents
. O tipo de getContents
é:
Se você conhece Node.js, isso é algo equivalente à Stream
process.stdin
,
exceto que você pode a tratar como uma String
normal e ela vai de pouco em
pouco sendo populada. Armazenamos na variável s
, mas nesse ponto o stdin
não
foi consumido. Há uma nota do autor original sobre isso no final do post.
Em seguida chamamos:
O que essa expressão faz é chamar lines s
para gerar outra lista preguiçosa de
cada uma das linhas do stdin
e chamar mapM_
com toda a nossa lógica sobre as
linhas.
mapM_
é um helper que executa uma função sobre uma collection no contexto de
um Monad
e omite o resultado. Podemos simplificar e dizer que é:
Inclusive, se você escrever isso o compilador vai inferir quase o tipo certo pra você, exceto um detalhe: algo que foi introduzido no GHC 7.10 esse ano, que deixa a função ser mais genérica e funcionar em outras estruturas além de listas; isso tem a ver com a proposta FTP (Foldable Traversable in Prelude), sobre a qual você pode ler aqui.
(avise se você gostaria de ler sobre isso)
Resta todaNossaLogica
que é:
Isso quer dizer para cada linha
em lines s
:
- Chama
scan mediaRegex linha
(retorna a lista de matches[(string, grupo)]
) - Passa isso pro
listToMaybe
(retorna umMaybe
para o primeiro elemento) - Passa isso para
fmap extractIfPresent
(veja o parágrafo abaixo) - Passa o resultado final (um tipo estruturado - o
Info
) parareportOnInfo
fmap
é uma função da type-class Functor
no Haskell. Muito resumidamente, um
Functor
é: uma estrutura de dados que contem um valor no qual podemos aplicar
uma função. Para isso temos o fmap
. Ele:
- “entra” dentro da estrutura
- aplica a função
- retorna outra estrutura de dados contendo o valor transformado
Então para um Maybe a
, fmap
recebe uma função de a -> a
, um Maybe a
e
retorna outro Maybe a
. Em outras palavras:
Usando Stack para publicar scripts standalone
Nós podemos testar nosso programa no REPL GHCi digitando main
ou :main
no
prompt do REPL e digitando linhas de entrada. Nós também podemos rodar
stack build
para compilar um binário nativo.
Uma outra opção é publicar o código-fonte como um script standalone de um arquivo. Isso pode ser muito conveniente em algumas circunstancias; você pode só confiar que o usuário tenha o Stack instalado.
Aqui está como podemos converter nosso programa em um script standalone: só
adicione essas duas linhas e faça o arquivo ser executável (chmod +x arquivo
):
O Stack vai ler o comando no comentário para: - Instalar o GHC se necessário - Instalar os pacotes listados - Interpretar o código
Nós especificamos uma distribuição LTS dos pacotes do Stackage para garantir que tudo quais versões de tudo serão usadas. (Nota: nesse caso, por causa da FFI com uma biblioteca escrita em C, ela deve ser instalada antes de rodar o script)
[N.T. Algo parecido é feito no meu projeto
stack-run-auto
com a adição de
não termos de especificar os pacotes ou a distribuição - eles são detectados
automaticamente]
Se você tem programas curtos que não precisam ser organizados em pacotes do Cabal completos, você pode tratar Haskell como uma “linguagem de scripting” e ainda ter acesso a todas as bibliotecas no Hackage!
Um aviso
Apesar dessa função do Stack como um interpretador de Haskell ser muito legal,
eu prefiro escrever código modular, em bibliotecas separadas e testáveis,
deixando o main
ser só a lógica que amarra as pontas de várias bibliotecas em
um módulo Main
do programa. Além disso, eu prefiro usar bibliotecas e
executáveis compilados porque eles tem um startup muito mais rápido. runghc
é um interpretador de Haskell, não um compilador nativo com otimizações. Claro
que a beleza do mundo do GHC é que você pode usar um ou o outro, e pular de
intepretado para compilado sem problemas.
O programa completo
Algumas notas adicionais
Uma limitação de um artigo expositório com código de exemplo é que nós não queremos desperdiçar espaço e atenção, e por isso tendemos a apresentar código rápido-e-sujo, ao invés de código com nível de produção (que é rápido, se recupera no caso de erros, tem boa documentação etc.). Eu tenho pensado no dilema de como não dar uma má impressão e ser um mau exemplo ao mostrar código simplista. Não há uma resposta fácil , mas eu senti que poderia ser útil prover notas opcionais “avançadas” as vezes, sobre como escrever Haskell no mundo real.
pcre-heavy
permite executar regexes contra String
s, ByteString
s e Text
s.
Na prática, para eficiencia, nós queremos usar
bytestring
e
text
o máximo possível,
no lugar do tipo String
, ineficiente. Um artigo Dia de Hackage de 2012 fala sobre o pacote text
.
Já que a biblioteca em PCRE escrita em C usada nos bastidores usa bytes, eu
geralmente uso bytestrings com o pcre-heavy
.
O código do main
no exemplo usa I/O preguiçoso para ler do input. Isso é
superficialmente muito elegante e conciso para propósitos pedagógicos, mas na
vida real isso é uma fonte de vazamentos de memória e outros problemas.
Inclusive faz as pessoas pensarem que “Haskell é ineficiente”. Para trabalho
real, eu gosto de usar o pacote pipes
,
que foi coberto em outro dia de Hackage de 2012
e também tem um
tutorial extensivo e bonito
por seu autor, Gabriel Gonzalez, que também tem um blog fantástico, ativo e de longa
data
“Haskell for all” que todos os Haskellers
deveriam seguir.
Finalmente, a expressão regular foi a escolha certa aqui? Foi simples o bastante para esse problema, mas você pode ver pelo pattern matching ad-hoc, strings hardcoded, o número de grupos e a frágil ordem posicional que as coisas ficariam suscetíveis a erros muito rápido se a expressão regular ficasse um pouco mais complexa e nós quisessemos tratar erros quando o match falhasse.
Conclusão
Suporte a regexes não é o ponto mais forte do ecossistema Haskell, que vai mais
para o lado de parsing estruturado, mas há opções se você quer mesmo usar
regexes, e eu gosto da família pcre-light
de bibliotecas de regex estilo-Perl
que agora incluí pcre-heavy
.
Eu também mostrei como adicionar duas linhas no topo de um programa Haskell para o transformar em um script do Stack.
Todo o código
Todo o código para a série estará nesse repositório do GitHub.