Haskell World logo Haskell WorldWrite code, build worlds
Open Source

How the Haskell Community Builds and Maintains Its Packages

Open source package maintenance is unglamorous work. The developers who do it reliably and well enable everyone else in the ecosystem to build confidently on stable foundations.

How the Haskell community builds and maintains its package ecosystem

Open source package maintenance is unglamorous work. The developers who do it reliably and well enable everyone else in the ecosystem to build confidently on stable foundations. The Haskell community has developed specific conventions, tools, and processes for this work over several decades, and understanding them is useful both for library consumers who want to evaluate package quality and for developers who want to contribute to the ecosystem.

Hackage: the central package repository

Hackage is the primary package repository for Haskell. Every package published there has a dedicated page with documentation generated from the package's Haddock comments, a changelog, version history, and dependency information. The revision system allows minor corrections to a published cabal file - fixing a version bound that turned out to be too strict - without republishing the full package. This is an important practical feature: dependency bound fixes can be deployed to Hackage quickly without requiring a new release.

Publishing to Hackage requires creating an account and registering your package. The first upload requires manual approval from a Hackage trustee; subsequent uploads of the same package are automatic. The package must have a valid cabal file, a Haddock-compatible source, and a license. Packages are expected to compile with recent GHC versions, and Hackage's CI infrastructure builds each uploaded package against a set of GHC versions to verify this.

Hackage's search function finds packages by name, and the documentation for each module is indexed, so you can find library functions without knowing which package they are in. The "reverse dependencies" view on each package page shows how many other packages depend on it, which is a rough signal of how widely-used and therefore how well-tested a package is.

Stackage: curated compatibility sets

Stackage maintains curated sets of Haskell packages - called resolvers - that are known to compile and test against each other. A Stackage resolver pins a specific GHC version and a set of package versions that are mutually compatible. This solves a specific pain point: when you have many dependencies, ensuring they all work together requires running the dependency solver, which sometimes fails or produces surprising version choices.

Stackage Nightly resolvers are updated daily, incorporating the latest versions of all included packages. LTS (Long Term Support) resolvers are updated weekly during the active period and then frozen, providing stability for projects that need to avoid frequent dependency updates. The LTS numbering reflects the GHC version: LTS 22.x resolvers use GHC 9.6, LTS 23.x uses GHC 9.8.

Getting a package included in Stackage requires submitting it to the Stackage repository and ensuring it compiles and tests with the current Nightly. The Stackage maintainers run automated builds that test all included packages against each other. When a new package upload would break another package in the set, the submitter is notified and must either fix the issue or wait until the affected package is updated.

Package versioning and the PVP

The Haskell community uses the Package Versioning Policy (PVP) for version numbering. PVP versions have the form A.B.C.D where A.B is the major version, C is the minor version, and D is a patch version. A major version bump (incrementing A or B) indicates a breaking API change. A minor version bump (incrementing C) indicates a non-breaking addition. A patch bump (incrementing D) indicates a bug fix.

Library consumers specify version bounds on their dependencies using Cabal's constraint syntax: my-library >= 1.2 && < 1.3. This says "any 1.2.x version of my-library." Using the PVP correctly means that this constraint does not break when my-library releases 1.2.1 with a bug fix (patch bump), but does not automatically allow 1.3.0 which might have breaking changes (minor bump in PVP major terms).

Maintaining sensible version bounds is an ongoing maintenance task. When a library adds new functions in a minor release, packages that use those new functions must update their lower bounds to reflect the minimum version that includes them. When a library makes a breaking change, all downstream packages must update their upper bounds and their code to work with the new API. The tooling helps: cabal outdated shows which dependencies have newer versions available, and Hackage's matrix build system shows which version combinations work together.

Writing Haddock documentation

Haddock is the standard documentation tool for Haskell. Documentation comments are placed above declarations and use a simple markup language: , | begins a documentation comment, links to other names use the @name@ syntax, inline code uses backticks, and paragraphs are separated by blank lines. Haddock generates HTML documentation from these comments, including the type signatures and the module structure.

Good library documentation explains not just what a function does but when to use it and when not to. For functions with subtle behavior - laziness effects, performance characteristics, edge cases - the documentation should address these explicitly. The haddock annotation @since 1.2.0@ marks which version a function was added in, which helps library users who need to maintain compatibility with older versions.

Module documentation, written at the top of each file, sets the context for the module's contents. A well-documented module explains what problem the module solves, how it fits into the library's overall design, and provides example usage. The Haskell standard library's modules are good examples of the documentation standard to aim for - they consistently explain both what functions do and how to use them effectively.

Testing practices in maintained packages

Well-maintained Haskell packages include test suites that run in CI on every commit. The test suite typically includes both unit tests (specific input-output examples) and property tests using QuickCheck (invariants that hold for all inputs). For parsing or serialization code, round-trip properties - encode followed by decode gives the original value - are standard and effective at catching bugs.

The cabal file's test-suite stanza declares the test suite's dependencies and entry point. Running the tests with cabal test compiles and runs the suite. CI configuration - typically a GitHub Actions workflow - runs the tests against multiple GHC versions. The matrix of GHC versions to test against is declared in the workflow file and typically covers the current stable version, the previous stable version, and sometimes GHC HEAD for early detection of compatibility issues with upcoming releases.

Test coverage is tracked informally in the community rather than through mandatory coverage tools. The important tests are the ones that cover the non-obvious behavior - edge cases, error conditions, the interactions between functions that are harder to reason about. Property tests naturally cover edge cases in the input domain by generating many random inputs; writing properties for the non-trivial invariants of your library's functions is the most effective way to build confidence in correctness.

Deprecation and migration

Deprecating a function without removing it gives users time to migrate before the breaking change lands. The Haskell pragma {-# DEPRECATED functionName "Use newFunctionName instead" #-} causes a compiler warning when the deprecated function is used, pointing users to the replacement. Keeping the deprecated function around for at least one major version release gives users a reasonable migration window.

Migration guides in the changelog explain what changed and what users need to do. For significant API changes, a well-written migration guide can reduce the burden of the change by orders of magnitude - users who would have spent hours figuring out what broke and why can follow the guide in minutes. The investment in writing a clear migration guide reflects the maintainer's consideration for the library's users, and it is visible in the quality of a project's changelog over time.

Long-term maintenance of a Haskell library is a community service. The developers who maintain widely-used packages - aeson, text, containers, servant - are providing infrastructure that enables the rest of the community to build. Contributing bug reports, pull requests, documentation improvements, or financial support through GitHub Sponsors to these maintainers is how the ecosystem sustains the common infrastructure it depends on.

SR
Sonja Reiter

Sonja Reiter contributed to three major open source projects and started writing documentation when she noticed most tooling guides were written by people who had never taught anyone to use them. She covers build tools, package ecosystems, and the invisible quality-of-life differences between development setups.

More posts by Sonja

More from the blog