Haskell Quality Gates
This skill covers the automation that keeps a Haskell project tidy: formatter, linter, performance defaults that prevent common pitfalls, and the pre-commit / CI / done-checklist that hold the line.
Formatter and linter
ormolu— zero-config, opinionated. No style debates. Always run with--mode inplacebefore commits, or--mode checkin CI.hlint— apply its suggestions by default unless they conflict witheffectfulpatterns or clarity.cabal-gild— formats.cabalfiles. Keeps build configs readable and consistently structured. Prefer overcabal-fmt.
The conventions are not negotiable: disagreement is settled by the tool, not by review.
Where these binaries come from. All three must be on PATH before any hook or CI step can call them — the pre-commit hooks below use language: system, which means "already installed". On the native path ormolu is pinned in mise.toml while hlint comes from Homebrew and cabal-gild from cabal install (see haskell-toolchain-mise); the devcontainer image ships ormolu and cabal-gild but not hlint or pre-commit, so install those inside the container on first use. Keep the ormolu CLI in the series HLS bundles, or editor formatting and the CLI will disagree.
Performance defaults
These are the type-and-strictness choices that prevent the most common Haskell performance pitfalls. Apply them by default; deviate only with a reason.
Text, neverString— for anything human-readable.Data.Textstrict by default;Data.Text.Lazyonly for streaming.ByteStringfor bytes — strict by default, same logic.Vectorfor indexed/numeric data —Data.Vectorboxed for arbitrary types,Data.Vector.Unboxedfor primitives.Map/HashMapinstead of association lists —containersfor ordered,unordered-containersfor hash-based.- Strict fields by default in data types —
data User = User { userId :: !UserId, userEmail :: !Email }. Laziness in fields is a frequent source of space leaks; opt into it explicitly with rare exceptions. BangPatternswhen a let-bound intermediate result should be forced.- Profile with
-prof -fprof-autoand visualize withghc-prof-flamegraph. Look at maximum residency with+RTS -s.
Pre-commit hooks
Use pre-commit (the Python framework, language-agnostic) to enforce formatting and linting locally. Commit .pre-commit-config.yaml to the repo:
# .pre-commit-config.yaml
repos:
- repo: local
hooks:
- id: ormolu
name: ormolu
entry: ormolu --mode inplace
language: system
files: \.hs$
- id: hlint
name: hlint
entry: hlint
language: system
files: \.hs$
- id: cabal-gild
name: cabal-gild
entry: cabal-gild
language: system
files: \.cabal$
Onboarding step (in README): pip install pre-commit && pre-commit install. After that, every git commit runs the hooks. ormolu --mode inplace reformats files; if anything changes, the commit is blocked and the user re-stages with git add.
CI safety net
Pre-commit hooks can be skipped with git commit --no-verify, and not every contributor will install them. Run the same checks in CI as a final gate:
# .github/workflows/ci.yml fragment
- name: Check formatting
run: ormolu --mode check $(git ls-files '*.hs')
- name: Check cabal formatting
run: cabal-gild --mode=check $(git ls-files '*.cabal')
- name: Lint
run: hlint src test
- name: Build
run: cabal build --ghc-options=-Werror
- name: Test (hspec + doctest)
run: cabal test
env:
HSPEC_OPTIONS: --color
- name: Documentation gate (coverage + references)
run: ./scripts/check-haddock.sh
Note -Werror is in CI but not in the .cabal file: locally, warnings should be visible but non-fatal during exploration; CI is where the line is held.
Before declaring work "done"
Always run, in order:
cabal build— clean build, no warnings. Warnings are configured in the.cabalfile (seehaskell-project-setup). Note that Haddock coverage is not enforced here: GHC has no-Wmissing-docs, so it is step 3's job (seehaskell-documentation).cabal test— all tests pass, including hspec, properties, and doctests. Output formatting is configured incabal.projectand.hspec(seehaskell-testing)../scripts/check-haddock.sh— Haddock generates cleanly at 100% coverage, with no broken references, malformed markup, or unresolved links. The script exists because neither-Wmissing-docs(GHC) nor-Werror(Haddock 2.31.1) is available on the pinned toolchain (seehaskell-documentation).hlint src test— no lint warnings (or all consciously suppressed with reason).
If pre-commit hooks are installed (pre-commit install), step 4 happens automatically on git commit. Ormolu formatting is handled automatically by the PostToolUse hook on each file edit.
When the user is stuck
Common debugging cues, with what to ask for next:
- Type errors: ask for the exact error message and a minimal reproducer. The error is almost always more useful than they realize once decoded. Translate it for them — GHC's vocabulary (
expected ... actual ..., "rigid type variable", "ambiguous occurrence") is dense but principled. - "Could not deduce": usually a missing constraint. Walk through where the constraint comes from — the compiler reports exactly which constraint is needed.
Show/Eqnot in scope: derived instances missing, or wrong import.- Space leaks: ask about
+RTS -soutput. Look for non-strict accumulators in folds (usefoldl'), missing strict fields, lazyMap.insertWith(useMap.insertWith'). - Library API unfamiliar or version-drifted: tell the user you'll check Hackage rather than guessing — Haskell library APIs drift and guessing wastes their time.
Related
- The warning set enforced in CI is defined in the
.cabalfile — seehaskell-project-setup. - The Haddock coverage gate (and why
-Wmissing-docs/haddock -Werrorare unavailable), doctest as a test suite:haskell-documentation. cabal testoutput formatting (test-show-details: direct,.hspec):haskell-testing.