Use Hpack

TL;DR

Use Hpack, and you don’t have to manually add/remove modules in the cabal file’s exposed-modules and other-modules section!

A little bit of background

Haskell has a very long history: Since 1987 which is long before the concept of “package managers.” When modern package management emerged, Haskellers had to invent something too. So they made “Cabal” which came to mean three things:

  1. A standard way to organize your project repository and describe the project metadata like project name, repo URL, dependencies, etc.
  2. A new file format to contain the aforementioned metadata. (YAML did not exist at the time or was not popular, I think.)
  3. A Haskell library to parse 2 and therefore manage 1

(This is why you should be careful about what someone means when they say “Cabal.”)

And then there is cabal-install which is different from “Cabal.” cabal-install is a command line application that lets you manage Cabal-based projects.

Sandbox

So, we had commands like ghc, ghci, and cabal-install, and we were happier than before. Until the programming community realized the problem of dependency hell; which package depends to which version of which library, etc. Naturally, we needed some kind of sandboxing per project, so we added that to cabal. That’s called Cabal sandbox. The problem was, although Cabal sandbox did solve the problem of dependencies-per-project,

  1. It still didn’t provide the compiler-per-project concept. If you are familiar with Python, consider how “virtualenv” and “pyenv” are different. The former is about isolating the version of dependency packages, while the latter is about the version of Python itself (CPython 3.6.1 or CPython 2.7 or PyPy 2, etc.)
  2. Sandbox alone could not solve the butterfly effect.
  3. It was quite slow because Cabal sandbox couldn’t share the pre-built binaries across different projects. Each project sandbox will begin from the zero base where most packages are not installed at all, and download/compile all libraries as they are added by the programmer. This means building “bytestring” five times if you have five projects. Note that you have a lot of packages that are used almost universally: bytestring, text, array, stm, unordered-containers, unix, directory, … All of them had to be built over and over again.

Stack

Stack solved these problems. Stack is actually a wrapper (plus some great deal of its own machinery) around the commands like ghc, ghci, and cabal-install. It does a brilliant job to isolate everything (both compilers and dependency packages) and sharing a binary if it can be shared.

For example, if you already have a bytestring-0.10 that was built using base-4.8, you don’t have to build it again when it’s needed because it’s cached. However, when you need base-4.9 and bytestring-0.10 in your project, you’ll have to build bytestring from scratch; this is exactly the behavior we want.

So what’s the problem?

One of the remaining problems is that the <projectname>.cabal file is in a custom format. Custom formats are generally not good because you can’t leverage the existing tools and free-ride other people’s work. Another pain point of the cabal files is that, whenever you add or remove some module (Haskell source code file) to/from your project, you have to edit the exposed-modules or other-modules section of the cabal file. Tedious, error-prone, and not so rewarding.

We understand that the concept of cabal file and its format came out before the popularization of YAML or TOML or anything, and we also understand that if Cabal decided to switch from <projectname>.cabal to package.yaml, it would be a breaking change. But maybe we can write some code to

  1. Have a package.yaml file
  2. Automatically transpile it to a cabal file whenever a cabal file is needed
  3. In the process, automate the exposed-modules/other-modules problem.

Fortunately, someone already did this. It’s called Hpack.

Conclusion

Use Hpack.

How?

Hpack is built into Stack, so there is no additional setup required. This is so brilliant.

If you already have a Haskell project, use hpack-convert. It will automatically generate a package.yaml file from your <projectname.cabal> file.

If you are starting a new Haskell project, use Haskeleton. It’s simple:

stack new haskeleton myprojectname -p 'github-username:xtendo-org' -p 'author-name:XT'

Update

eacameron says:

Actually, my truly favorite thing about hpack is that you can specify a common set of dependencies that get shared across all the libraries/exes/tests/benchmarks in your setup. That part is truly annoying in cabal files.

You can do the same thing with ghc-options, language extensions, etc.

I totally agree.