coldwa.st
Todos os guiasProgramaçãoWebDadosFerramentasBases de dadosHaskellConceitosCabal e buildsToolchainCompiladorDesempenhoEditor e HLS

Haskell · Cabal · gestão de pacotes

Como se adiciona uma dependência no Cabal?

Por ColdwastAtualizado a 4 de agosto de 20267 min de leitura#haskell#cabal#pacotes
Altas pilhas de livros usados a encher o corredor de uma livraria
Pilhas de livros usados atulham o corredor de uma livraria, empilhadas mais alto do que as estantes ao fundo.

A resposta curta é que uma dependência não se adiciona com um comando, mas com uma linha num ficheiro. Um pacote Haskell declara aquilo de que precisa no seu ficheiro .cabal, sob build-depends, e tudo o resto decorre dessa declaração.

Vale a pena dizê-lo com clareza, porque quem chega do npm ou do Cargo procura um comando install que altere um manifesto, e depois pergunta-se porque nada do que instalou está visível para o compilador.

O campo que realmente importa: build-depends

Abra o seu ficheiro .cabal e encontre a estrofe daquilo que está a construir: uma library, um executable ou uma test-suite. Cada uma traz o seu próprio build-depends, e é esse o ponto que mais escapa.

library
  exposed-modules:  MyApp.Core
  build-depends:    base ^>=4.18
                  , containers ^>=0.6
                  , text ^>=2.0
  default-language: Haskell2010

Adicionar um pacote é acrescentar uma linha aqui. A vírgula no início da linha é uma convenção, não uma exigência: existe para que acrescentar ou remover uma linha toque apenas numa.

As dependências são de cada estrofe. Um pacote de que a sua suite de testes precisa não pertence ao build-depends da biblioteca, e um pacote de que a sua biblioteca precisa não fica automaticamente disponível para o seu executável, a menos que este dependa da própria biblioteca. É mais estrito do que na maioria dos ecossistemas, e é deliberado: mantém as ferramentas de teste fora daquilo que os seus utilizadores têm de compilar.

O que o operador caret significa mesmo

O ^>= que se vê por toda a parte não é decorativo. É uma abreviatura ligada à Package Versioning Policy de Haskell, a PVP, e compreendê-la elimina quase toda a adivinhação sobre limites.

Sob a PVP, uma versão lê-se A.B.C.D, em que A.B em conjunto formam a versão maior. As alterações que quebram incrementam A ou B. As adições que não podem quebrar código existente incrementam C. Assim, ^>=1.2.3 significa pelo menos 1.2.3 e abaixo da maior seguinte, o que se expande para >=1.2.3 && <1.3.

A consequência apanha muita gente desprevenida: em Haskell, passar de 1.2 para 1.3 é um salto maior, não menor. Se trouxer o modelo mental do versionamento semântico, onde só o primeiro número é maior, escreverá limites bem mais largos do que pretendia.

Tábuas de madeira idênticas montadas em pequenas estruturas, cada camada assente na de baixo
Tábuas de madeira idênticas montadas em pequenas estruturas, cada camada assente na de baixo.

Porque o cabal update vem primeiro

O Cabal resolve dependências contra uma cópia local do índice de pacotes do Hackage. Se essa cópia estiver desatualizada, um pacote publicado na semana passada simplesmente não existe para a sua máquina, e obtém um erro de resolução que se lê como se o nome do pacote estivesse errado.

cabal update
cabal build

Execute cabal update quando um pacote que sabe existir não for encontrado, e após qualquer intervalo longo entre sessões. É a primeira coisa a tentar e não custa nada.

Ler uma falha de resolução sem pânico

Quando o resolvedor não consegue satisfazer todas as restrições ao mesmo tempo, comunica o conflito em vez de escolher em silêncio. A mensagem é densa, mas diz algo concreto: duas das suas dependências querem intervalos incompatíveis de uma terceira.

As opções honestas são poucas, e convém conhecê-las antes de recorrer a uma flag:

Aliviar um limite que foi você a pôr. Se o limite demasiado estreito está no seu próprio ficheiro .cabal, alargá-lo é legítimo, desde que compile e teste depois, em vez de o presumir.

Escolher outra versão do pacote que está a adicionar. Muitas vezes uma versão mais antiga da nova dependência encaixa nas restrições que o projeto já tem.

Usar --allow-newer com consciência e a título temporário. Diz ao resolvedor para ignorar os limites superiores declarados por outros pacotes. Esses limites foram escritos por mantenedores que tinham uma razão, por isso trate-o como um diagnóstico que mostra onde está o conflito real, não como uma correção que se faz commit.

Fixar aquilo que resolveu

A resolução escolhe versões num dado momento. Para tornar esse momento reproduzível, fixe-o:

cabal freeze

Isto escreve um ficheiro cabal.project.freeze que regista as versões exatas escolhidas. Faça commit dele quando quiser uma compilação que se comporte da mesma forma no mês seguinte, que é a expectativa habitual para uma aplicação. As bibliotecas são o caso oposto: devem permanecer flexíveis para que quem depende delas possa resolver livremente.

Para um projeto que abranja vários pacotes, ou que precise de uma dependência de código-fonte ausente do Hackage, essa configuração pertence ao cabal.project e não ao ficheiro .cabal. Manter os dois distintos é a forma mais clara de se orientar: o ficheiro .cabal descreve um pacote, o cabal.project descreve como compilar um conjunto deles.

Em resumo

Adicione uma dependência acrescentando uma linha a build-depends, na estrofe que dela precisa de facto. Limite-a com ^>=, lembrando que sob a PVP os dois primeiros números formam em conjunto a versão maior. Execute cabal update antes de culpar um pacote por não existir. Fixe as aplicações, deixe as bibliotecas soltas.

Se ainda está a escolher ferramentas, a nossa comparação entre Stack e Cabal trata dessa decisão, e o guia de instalação do GHCup monta a toolchain que este artigo assume já instalada.

O campo build-depends, a expansão do operador caret e a regra da versão maior A.B seguem o guia de utilizador do Cabal e a Package Versioning Policy de Haskell, verificados no momento da escrita. A disponibilidade dos comandos varia com a sua versão do cabal-install: execute cabal --version e consulte o guia da sua versão antes de depender de um subcomando específico.