Você já parou para pensar como seria um arquivo Cabal minimalista? Em outras palavras, qual é a menor quantidade de dados que você precisa incluir em um arquivo Cabal para gerar como resultado um pacote Haskell completamente funcional (trocadilho não-intencional)?

Grosseiramente, o Cabal é para o Haskell o que o Maven é para o Java; o pip é para o Python; o NPM é para o Node.js; o Bundler é para o Ruby; e o CMake é para o C.

Independente da linguagem em que você programa, com certeza existe alguma coisa que desempenha a função do Cabal… Mas será que você conhece suficientemente bem as ferramentas que você usa?

Eu comecei a refletir sobre isso após ler este post do Juan Pedro Isaza (em inglês) e perceber que, embora eu seja um programador Haskell há mais de 6 anos, eu nunca parei para entender a fundo como funciona o Cabal — uma ferramenta que eu uso diariamente.

É por isso que eu me dei o trabalho de fazer este post, para encorajá-lo a repetir o experimento que nós faremos aqui com as tecnologias que você usa no seu dia-a-dia.

Neste artigo, nós vamos gerar um arquivo .cabal mínimo, mas eu deixo para você o desafio de descobrir como seria o pom.xml mínimo, o requirements.txt mínimo, o CMakeLists.txt mínimo… e postar o resultado aqui nos comentários! :+1:

Vá Direto ao Assunto…

O Cabal e o Haskell

Kabal - Mortal Kombat III

Se você pensou que este artigo era sobre Mortal Kombat, eu sinto desapontá-lo…

O Cabal é um sistema de build e empacotamento de bibliotecas e programas Haskell. Ele define uma interface comum para que os pacotes possam ser facilmente compilados e distribuídos de maneira portável.

Especificamente, o Cabal descreve o que é um pacote Haskell, como esses pacotes interagem com a linguagem e os recursos que as diferentes implementações do Haskell devem contemplar para que os pacotes funcionem corretamente.

Dessa forma, todo pacote Haskell tem um arquivo com a extensão .cabal contendo metadados sobre ele. Esse arquivo é processado pelo executável cabal, que nós usamos para fazer o build, executar e, até mesmo, publicar os nossos pacotes.

Ambiente e Metodologia

Nós faremos alguns testes para descobrir como é um arquivo Cabal minimalista, mas antes de continuarmos, anota aí as versões do Cabal e do GHC que eu estou utilizando:

1
2
3
4
5
$ cabal --version
cabal-install version 1.24.0.2
compiled using version 1.24.2.0 of the Cabal library
$ ghc --version
The Glorious Glasgow Haskell Compilation System, version 8.0.2

Nossa metodologia será a seguinte:

  1. Partindo do absoluto zero, nós aplicaremos pequenas mudanças de forma incremental.
  2. Toda vez que fizermos algo, por menor que seja, tentaremos fazer o build do pacote.
  3. Para cada mensagem de erro obtida, nós realizaremos a menor quantidade de trabalho possível para consertar o erro.

O Experimento

Vamos começar criando um diretório experiments onde faremos os nossos testes:

1
2
$ mkdir experiments
$ cd experiments/

E vamos tentar fazer o build:

1
2
3
4
$ cabal build
Package has never been configured...
cabal: No Cabal file found.
Please create a package description file <pkgname>.cabal

Oops! Temos um erro: “arquivo Cabal não encontrado” — o que era esperado, já que executamos o comando em um diretório vazio. Seguindo o princípio do menor esforço possível, vamos então criar um arquivo Cabal vazio, claro.

1
$ touch experiments.cabal

E tentar fazer o build:

1
2
$ cabal build
... Using 'build-type: Custom' but there is no Setup.hs...

A mensagem de erro menciona algo sobre um tipo de build “customizado”, mas nós queremos um build simples, então vamos ser específicos sobre isso:

1
$ echo "build-type: Simple" >> experiments.cabal

E tentar de novo:

1
2
3
$ cabal build
... No 'name' field.
... No 'version' field.

Todo pacote Haskell deve ter um nome e uma versão, então vamos adicionar essas informações no arquivo:

1
2
$ echo "name: experiments" >> experiments.cabal
$ echo "version: 1.0.0" >> experiments.cabal

Essa mudança conserta alguns erros, mas não todos:

1
2
$ cabal build
... No executables, libraries (...) found. Nothing to do.

Nós ainda temos que pedir para o Cabal fazer alguma coisa, como fazer o build de algum executável ou biblioteca. Por enquanto, vamos declarar uma seção library (biblioteca):

1
$ echo "library" >> experiments.cabal

E tentar de novo:

1
2
3
$ cabal build
... A package using section syntax must specify at least
... 'cabal-version: >= 1.2'.

Adicionar uma seção library requer pelo menos a versão 1.2 do Cabal, mas antes de especificarmos uma versão do Cabal, vamos primeiro dar uma olhada no arquivo que construímos até aqui:

1
2
3
4
5
$ cat experiments.cabal
build-type: Simple
name: experiments
version: 1.0.0
library

Infelizmente, a partir de agora, não dá mais para continuar fazendo o append (>>) de coisas no arquivo (pode tentar aí que só vai dar erro). Então nós teremos que abrir um editor de texto para adicionar a restrição mínima de versão do Cabal.

Após reorganizar o arquivo, ele deve ficar assim:

1
2
3
4
5
6
name: experiments
version: 1.0.0
build-type: Simple
cabal-version: >= 1.2

library

Vamos tentar o build:

1
2
3
4
5
$ cabal build
Resolving dependencies...
Configuring experiments-1.0.0...
Building experiments-1.0.0...
Preprocessing library experiments-1.0.0...

Voilà! Funciona!

Quer dizer, na verdade, funciona mais ou menos…

Tente abrir o GHCi:

1
2
3
$ cabal repl
... Not in scope: ‘System.IO.hSetBuffering’
... No module named ‘System.IO’ is imported.

O erro diz que a função hSetBuffering do módulo System.IO está fora de escopo e/ou que nenhum módulo com esse nome foi importado.

E é verdade, pois nós não adicionamos o pacote base (que contém funcionalidades elementares do Haskell) como uma dependência do projeto:

1
2
3
4
5
6
7
name: experiments
version: 1.0.0
build-type: Simple
cabal-version: >= 1.2

library
  build-depends: base

Vamos tentar mais uma vez:

1
2
3
4
$ cabal repl
Warning: No exposed modules
...
Prelude>

Já tá ficando chato…

Dessa vez nós conseguimos abrir o GHCi, mas não existe nenhum… “Módulo exposto”?

Bom, o código Haskell que nós eventualmente escreveremos terá que ser colocado em algum lugar (ou módulo), então vamos adicionar um módulo Experiments na lista de módulos expostos:

1
2
3
4
5
6
7
8
name: experiments
version: 1.0.0
build-type: Simple
cabal-version: >= 1.2

library
  exposed-modules: Experiments
  build-depends: base

E criar o arquivo correspondente — Experiments.hs — com o seguinte conteúdo:

1
module Experiments where

Agora vai… :pray:

1
2
3
$ cabal repl
...
*Experiments>

Parece que funcionou!! Quer dizer, mais ou menos (de novo?! :sleepy:)…

Só por diversão, vamos tentar acrescentar um componente executável:

1
2
3
4
5
6
7
8
9
10
11
name: experiments
version: 1.0.0
build-type: Simple
cabal-version: >= 1.2

library
  exposed-modules: Experiments
  build-depends: base

executable experiments
  main-is: Main.hs

A seção executable declara um executável chamado experiments e informa que o arquivo Haskell que contém a função main é o Main.hs. Vamos criar o Main.hs com o seguinte conteúdo:

1
main = putStrLn "Put it to the test!"

Embora nós não tenhamos declarado nenhuma dependência para o executável, parece que está tudo funcionando:

1
2
3
$ cabal run
...
Put it to the test!

Mas tem um detalhe: geralmente quando um executável e uma biblioteca são implementados dentro do mesmo pacote, o primeiro depende do segundo. Então vamos adicionar essa relação usando a propriedade build-depends:

1
2
3
4
5
6
7
8
9
10
11
12
name: experiments
version: 1.0.0
build-type: Simple
cabal-version: >= 1.2

library
  exposed-modules: Experiments
  build-depends: base

executable experiments
  main-is: Main.hs
  build-depends: experiments

E fazer o build:

1
2
3
4
$ cabal build
... The field 'build-depends: experiments' refers to a library which
... is defined within the same package. To use this feature the
... package must specify at least 'cabal-version: >= 1.8'.

WHAT?! :scream:

Aparentemente, o recurso que permite que um executável referencie uma biblioteca definida no mesmo pacote só foi implementado a partir da versão 1.8 do Cabal.

Fazer o quê, né?! Vamos atualizar o campo cabal-version:

1
2
3
4
5
6
7
8
9
10
11
12
name: experiments
version: 1.0.0
build-type: Simple
cabal-version: >= 1.8

library
  exposed-modules: Experiments
  build-depends: base

executable experiments
  main-is: Main.hs
  build-depends: experiments

E tentar de novo:

1
2
3
$ cabal build
... Failed to load interface for ‘Prelude’
... It is a member of the hidden package ‘base-4.9.1.0’.

You gotta be kidding… Não é possível que quebrou tudo!

Pela mensagem de erro, nós apenas esquecemos de adicionar o base como dependência do executável… Mas por que raios funcionou antes?

Acontece que antes da versão 1.8 do Cabal, as dependências eram globais, ou seja, o executável tinha acesso ao base porque ele estava listado como uma dependência da biblioteca (na seção library).

Agora parece que nós temos que adicionar o base nos dois lugares:

1
2
3
4
5
6
7
8
9
10
11
12
name: experiments
version: 1.0.0
build-type: Simple
cabal-version: >= 1.8

library
  exposed-modules: Experiments
  build-depends: base

executable experiments
  main-is: Main.hs
  build-depends: base, experiments

Se você tentar de novo, você vai ver que funciona! Na verdade, quase… Se nós não especificarmos uma linguagem padrão, o Cabal usará o Haskell 98, mas a que nós queremos é o Haskell 2010, certo?

Bom, vamos checar se isso realmente acontece adicionando um tipo de dados vazio no módulo Experiments:

1
2
3
module Experiments where

data Empty

A menos que você adicione a extensão de linguagem -XEmptyDataDecls, esse código não deveria funcionar no Haskell 98:

1
2
$ cabal build
... ‘Empty’ has no constructors (EmptyDataDecls permits this)

Mas ele funciona no Haskell 2010, então sejamos explícitos com o Cabal:

1
2
3
4
5
6
7
8
9
10
11
12
13
name: experiments
version: 1.0.0
build-type: Simple
cabal-version: >= 1.8

library
  exposed-modules: Experiments
  build-depends: base
  default-language: Haskell2010

executable experiments
  main-is: Main.hs
  build-depends: base, experiments

Fazendo o build:

1
2
3
$ cabal build
... To use the 'default-language' field the package needs to
... specify at least 'cabal-version: >= 1.10'.

Céus!! Toda hora uma novidade…

O erro diz que o campo default-language, onde nós especificamos a linguagem como sendo “Haskell2010”, precisa da versão 1.10 do Cabal…

Mas ei, espera aí! A versão 1.10 não é a que obtemos quando usamos cabal init para inicializar nossos projetos com um arquivo Cabal?

Curiosamente, sim! E com essa mudança nós chegamos à versão final do nosso arquivo. Observe que “Haskell2010” é especificada para todos os componentes:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
name: experiments
version: 1.0.0
build-type: Simple
cabal-version: >= 1.10

library
  exposed-modules: Experiments
  build-depends: base
  default-language: Haskell2010

executable experiments
  main-is: Main.hs
  build-depends: base, experiments
  default-language: Haskell2010

Senhoras e senhores, este é o menor arquivo Cabal que resulta em um pacote Haskell completamente funcional contendo uma biblioteca e um executável (que nós adicionamos apenas por diversão).

Conclusão

Este artigo foi um poço de surpresas e diversão. Tanto que o título não foi à toa:

Cabalístico

Que é ou tem significado oculto, secreto ou misterioso; enigmático, incompreensível.

Mas não se deixe enganar achando que tudo sobre Haskell é demasiadamente complexo. Hoje em dia, com ferramentas como o Stack e o hpack, raras serão as vezes em que você terá que manipular um arquivo Cabal diretamente.

Por fim, eu espero ter conseguido despertar em você a curiosidade de replicar esse experimento em qualquer que seja a sua tecnologia favorita… E ah, quando você fizer isso, não esquece de postar o resultado aqui nos comentários, beleza? :+1:

Um forte abraço! Vejo você no próximo post!

Comentários desabilitados...