Haskell package checklist

by Taylor Fausak on

This post covers everything you need to know about developing a Haskell package. I wrote it because I made a few packages and nothing covers the entire process. The Cabal user guide provides good low-level information, and Sebastiaan Visser’s Towards a better Haskell package gives some nice high-level guidance. This post covers both of those and fills in some of the gaps left by them.

  • Use Git for source control. If it’s not in source control, it doesn’t exist. Git is the most popular choice, but its interface can be difficult to understand. Consider using a GUI like GitHub Desktop.

  • Host on GitHub. GitHub allows other developers to easily contribute to your package. Compared to other hosts, you are more likely to receive contributions. Also GitHub integrates nicely with many other services.

  • Build with Stack. Stack painlessly manages Haskell dependencies. It installs GHC for you and ensures you get a build plan that actually works. If your package builds today, Stack ensures it will continue to build tomorrow.

  • Define with hpack. The default Cabal package file format is custom, tedious, and verbose. hpack uses YAML and avoids unnecessary boilerplate. Stack integrates hpack to automatically convert your package.yaml into a *.cabal file when necessary.

  • Name with kebab-case. Keep everything lowercase to avoid confusion. You don’t want people trying to install http when they really meant HTTP. Use hyphens to separate words, but keep it short and memorable. Nobody wants to type out hypertext-transfer-protocol.

  • Use Semantic Versioning. Unfortunately Hackage recommends the Package Versioning Policy. The PVP adds ambiguity by using two major version numbers. That encourages packages to stay on major version 0, which doesn’t inspire confidence. Many other languages use Semantic Versioning. SemVer matches how developers generally think about version numbers.

  • License your package. Nobody can use a package without a license. The most popular license for Haskell is BSD 3-Clause, followed by MIT. Whichever license you choose, include the license file in your package.

  • Write a README. Most people will familiarize themselves with your package by reading your README. It should describe the problem that your package solves. Be sure to include at least one concrete example in it. GitHub will automatically format your README.markdown.

  • Keep a change log. Most packages will use Git tags to mark releases, but reading diffs is not an acceptable way for users to discover changes. Put a human-readable summary of changes in the GitHub releases. Then link to that from your CHANGELOG.markdown.

  • Write a synopsis. This shows up when searching and viewing your package. Keep it short, imperative, and descriptive. Also write a description, which can be pretty much the same as the synopsis. For example:

    name:        aeson
    synopsis:    Encode and decode JSON.
    description: Aeson encodes and decodes JSON.
    
  • Avoid heavy dependencies. Only add a dependency if your package needs it to function. Don’t include stuff that’s just nice to have. For example, you should probably avoid lens even though it makes code easier to write. In addition to avoiding heavy dependencies, you should avoid having too many dependencies. Think about how long it would take to install your package starting from scratch.

  • Include extra-source-files. If a file is necessary for your package to build, it belongs in extra-source-files. This includes files that tests and benchmarks need. You should also include package metadata like your README and change log. Note that you don’t need to include your license file if your package’s license-file is set.

  • Fix package warnings. Stack prints these out when you run stack sdist. You should fix all of them, even though some aren’t that useful. For instance, Stack warns you if you don’t set a category even though the categories on Hackage aren’t that useful. To fix warnings about version bounds, consider using --pvp-bounds=both.

  • Put Haskell files in source/. In other words, separate your package metadata from actual source files. This makes it easy to write scripts that work on every Haskell file, like formatting or counting lines of code. The exact names aren’t important, but you should end up with a structure like this:

    • source/library/YourPackage.hs
    • source/executables/Main.hs
    • source/tests/Main.hs
  • Match package and module names. If your package is named tasty-burrito, you should have a top-level module called TastyBurrito. Avoid the unnecessary module hierarchy like Data.ByteString or Text.ParserCombinators.Parsec. While the package name uses kebab-case, module names should use CamelCase.

  • Require one import. Users should be able to get started with nothing more than import YourPackage. If necessary, re-export stuff from other packages. Design for qualified imports; don’t be afraid to take common names like singleton.

  • Expose implementation details. People will want to use your package in ways you didn’t think of. Make everything public, but not necessarily part of your published API. Use Internal module names like Data.Text.Internal to signal that things aren’t published.

  • Build -Wall clean. GHC finds all kinds of problems with -Wall. Few of them are false positives and they generally help you write better code. But if you don’t like one, disable it with -fno-warn-whatever. Be sure to use stack build --pedantic when developing your package. It will force you to fix warnings.

  • Follow most HLint suggestions. Overall HLint is a great tool for improving code quality. However some suggestions aren’t worth following. For example, the re-export shortcut suggestion breaks the Haddock documentation. Use your own judgment when deciding which suggestions to follow.

  • Format code with hindent. hindent is the closest thing we have to a community style. Using it frees you from ever thinking about formatting again. If you don’t like how something looks, fix it in hindent and everyone’s formatting will improve. Using hindent also avoids pointless arguments about style in pull requests.

  • Write documentation with examples. Types are not a substitute for documentation. Neither are laws. Usually functions are added to solve a specific problem. Show that problem in the documentation as an example.

  • Test with Tasty using Hspec. Tasty provides a framework for running different kinds of tests with the same command line interface. It handles randomizing the test order, selecting which tests to run, and displaying their output. Hspec provides a library for writing human-readable tests. Use other testing libraries like QuickCheck when they make sense.

  • Run tests on Travis CI. Travis CI is free for open source projects and integrates with GitHub. Every time you push a commit to GitHub, Travis CI will run your test suite. This makes it easy to keep your package buildable. By default Travis CI runs on Linux, but you can also run on macOS. If you want to run your test suite on Windows, consider using AppVeyor.

  • Keep executables small. If your package provides an executable, define it in your library and re-export it. This allows other packages to use your executable’s behavior from Haskell. Your executable should look like this:

    module Main (module YourPackage) where
    import YourPackage (main)
    
  • Benchmark with Criterion. If your package needs to be fast, Criterion is the best tool for measuring it. On the other hand if your package doesn’t need to be fast, there’s no sense in maintaining benchmarks for it.

  • Automate releases. Don’t manually create distribution tarballs and upload them to Hackage. Instead, get Travis CI to do it. Travis CI sets the TRAVIS_TAG environment variable. If that’s set, you can run stack upload . to upload your package. Travis CI will need your Hackage credentials, so be sure not to leak those into the build log.

If you’re looking for a starting point that ticks most of these boxes, consider my Haskeleton Stack template. It will give you a good base to start from. If you’re looking for an actual package that follows the guidelines, check out Rattletrap, my Rocket League replay parser and generator. It can show you exactly how some of these things are implemented.