A Haskell [C]hecklist. For starting off, rebooting, or just freshening up.
A new GHC and a new GHC2024 needs a refresh of the Haskell [C]hecklist. Highlights include:
- GHC2024 is here and a GHC2024 cabal stanza backport is included.
- Given the spread of the community, CI recommendations cover ghc-8.6 to ghc-8.12.
- cabal-gild, a cabal file formatter, gets included in the checklist.
The checklist has been tested with:
ghcup list -c set -r
A very, very short list for creating and maintaining a Haskell project.
- [ ] Install Haskell via ghcup. Install everything.
- [ ] Create a new directory and run cabal init. Follow the prompts.
- [ ] (optional) Install a few more tools.
- [ ] Pick an IDE; vscode, neo-vim, emacs or unix command line. It all works.
- [ ] (optional) read through the extras section.
- [ ] (optional) read through the cabal stanzas section.
- [ ] (required) hack happy Haskell for your new pet project.
- [ ] follow the checklist when you want to tidy up.
Welcome to Haskell2024
- [ ] version bump
- [ ] ghcup upgrades
Check if tooling is current, and upgrade with ghcup as needed.
ghcup list -c set -r
- [ ] cabal.project check
Is the cabal.project & cabal.project.local files clean?
cat cabal.project
cat cabal.project.local
- [ ] upstream publishings
Have upstream dependencies been published on Hackage?
- [ ] cabal update
cabal update
- [ ] cabal outdated
cabal outdated
- [ ] cabal gen-bounds
cabal gen-bounds
- [ ] cabal build –ghc-options=-Wunused-packages
cabal clean && cabal build --ghc-options=-Wunused-packages
- [ ] cabal formatter
cabal-gild -m check --input=xyzzy.cabal
cabal-gild --io=xyzzy.cabal
- [ ] pragma cleanup
Consider a backported GHC2024.
- [ ] doctests
cabal-docspec
cabal run doctests
- [ ] cabal install
cabal install
- [ ] cabal bench
cabal bench
- [ ] ormolu
ormolu --mode check $(git ls-files '*.hs')
ormolu --mode inplace $(git ls-files '*.hs')
- [ ] CI upgrade
- check tested-with line in cabal file
- check ./.github/workflow/haskell-ci.yaml actions for updates
- [ ] ChangeLog
- [ ] Versioning
Check current hackage version and bump the package version in the cabal file, based on PvP.
- [ ] branch, push & check CI
- [ ] stack checks
https://github.com/commercialhaskell/stackage/blob/master/verify-package
stack init --resolver nightly --ignore-subdirs stack build --resolver nightly --haddock --test --bench --no-run-benchmarks
- [ ] haddock
cabal haddock
- [ ] readme
- [ ] PR to main
- [ ] merge PR
- [ ] immediate checkout and pull main
- [ ] final check
cabal clean && cabal build && cabal-docspec
- [ ] hkgr tagdist
hkgr tagdist
- [ ] hkgr publish
hkgr publish
This won’t work if there are cabal.project specifications. So, something like:
cabal upload .hkgr/prettychart-0.2.0.0.tar.gz --publish
- [ ] check Hackage
Sometimes haddocks don’t build on Hackage. Here’s a recipe for uploading your own docs.
cabal haddock --builddir=docs --haddock-for-hackage --enable-doc cabal upload -d --publish docs/*-docs.tar.gz
To quickly create a new Haskell project, run `cabal init` interactively or look through the cabal docs and use the command line eg
mkdir minimal && cd minimal && cabal init --minimal --simple --overwrite --lib --tests --language=GHC2024 --license=BSD-2-Clause -p minimal
[Info] Using cabal specification: 3.0 [Info] Creating fresh file LICENSE... [Info] Creating fresh file CHANGELOG.md... [Info] Creating fresh directory ./src... [Info] Creating fresh file src/MyLib.hs... [Info] Creating fresh directory ./test... [Info] Creating fresh file test/Main.hs... [Info] Creating fresh file minimal.cabal... [Warn] No synopsis given. You should edit the .cabal file and add one. [Info] You may want to edit the .cabal file and add a Description field.
A quick test of these installations is to compile and test the project using cabal:
cd minimal && cabal build && cabal test
Setup of a modern Haskell environment is straight forward. ghcup takes care of ghc, cabal, stack & the haskell-language-server. cabal
can then be used to install other tools.
ghcup list -c set -r
ghcup
places everything in ~/.ghcup/bin
which cabal
/Users/tonyday567/.ghcup/bin/cabal
Haskell-language-server versions matching older GHC versions are also installed, and selected automatically.
haskell-language-server-wrapper --version
haskell-language-server version: 2.5.0.0 (GHC: 9.2.8) (PATH: /Users/tonyday567/.ghcup/hls/2.5.0.0/lib/haskell-language-server-2.5.0.0/bin/haskell-language-server-wrapper)
This guide uses the following tools, which, when used together, provide the modern Haskell experience:
Most of the tools can be installed via cabal:
cabal install ormolu hlint hkgr cabal-gild --allow-newer --overwrite-policy=always
ghciwatch is via our cousins at rust:
cargo install ghciwatch
cabal
stores executables in ​~​/.cabal/bin, stack
in ​~​/.local/bin.
which hlint
/Users/tonyday567/.cabal/bin/hlint
cabal-docspec is a doctest runner that exists as a process outside the specification of a cabal project, acting more like hlint then a separate cabal stanza. The project is not available on hackage and needs to be installed manually:
git clone https://github.com/phadej/cabal-extras
cd cabal-extras/cabal-docspec
cabal install cabal-docspec:exe:cabal-docspec --overwrite-policy=always
A project typically needs a few more files that cabal init
doesn’t cover.
Practice varies widely, from saying nothing to all documentation being in the readme. This readme.md template:
- adds some badges for Hackage & CI.
- Includes a short description and basic Usage example, which in many cases should be exactly repeated in the cabal file as synopsis and description stanzas.
{{{name}}}
===
[](https://hackage.haskell.org/package/{{{name}}})
[](https://github.com/{{{github-username}}}/{{{name}}}/actions?query=workflow%3Ahaskell-ci)
`{{{name}}}` is a new package.
Usage
==
``` haskell
import {{{lib-name}}}
```
An alternative readme approach.
* {{{name}}}
[[https://hackage.haskell.org/package/{{{name}}}][https://img.shields.io/hackage/v/{{{name}}}.svg]]
[[https://github.com/{{{github-username}}}/{{{name}}}/actions?query=workflow%3Ahaskell-ci][https://github.com/{{{github-username}}}/{{{name}}}/workflows/haskell-ci/badge.svg]]
~{{{name}}}~ is a new package.
* Usage
#+begin_src haskell :results output
import {{{lib-name}}}
#+end_src
* Development
#+begin_src haskell :results output
:set -Wno-type-defaults
:set -Wno-name-shadowing
:set -XOverloadedStrings
#+end_src
check
#+begin_src haskell :results output :export both
let x = "ok"
putStrLn x
#+end_src
- ignore: {name: Use if} - ignore: {name: Use bimap} - ignore: {name: Eta reduce}
:set -Wno-type-defaults
/.stack-work/
/dist-newstyle/
stack.yaml.lock
**/.DS_Store
cabal.project.local*
/.hie/
.ghc.environment.*
/.hkgr/
GitHub actions are the current and common practice for continuous integration of projects. The CI file below uses actions from haskell-actions. It includes tests for ormolu, hlint, cabal-doctest and the usual cabal checks across a wide GHC range.
GitHub Actions Documentation - GitHub Docs
As at publication, the latest actions are:
- actions/checkout@v4
- actions/cache/restore@v4
- haskell-actions/setup@v2
- haskell-actions/hlint-setup@v2
- haskell-actions/run-ormolu@v17
- haskell-actions/setup@v2
name: build
on: [push]
# INFO: The following configuration block ensures that only one build runs per branch,
# which may be desirable for projects with a costly build process.
# Remove this block from the CI workflow to let each CI job run to completion.
concurrency:
group: build-${{ github.ref }}
cancel-in-progress: true
jobs:
hlint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: haskell-actions/hlint-setup@v2
- uses: haskell-actions/hlint-run@v2
with:
path: .
fail-on: warning
ormolu:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: haskell-actions/run-ormolu@v17
build:
name: GHC ${{ matrix.ghc-version }} on ${{ matrix.os }}
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest]
ghc-version: ['9.12', '9.10', '9.8', '9.6']
include:
- os: windows-latest
ghc-version: '9.12'
- os: macos-latest
ghc-version: '9.12'
steps:
- uses: actions/checkout@v4
- name: Set up GHC ${{ matrix.ghc-version }}
uses: haskell-actions/setup@v2
id: setup
with:
ghc-version: ${{ matrix.ghc-version }}
- name: Configure the build
run: |
cabal configure --enable-tests --enable-benchmarks --disable-documentation
cabal build --dry-run
# The last step generates dist-newstyle/cache/plan.json for the cache key.
- name: Restore cached dependencies
uses: actions/cache/restore@v4
id: cache
env:
key: ${{ runner.os }}-ghc-${{ steps.setup.outputs.ghc-version }}-cabal-${{ steps.setup.outputs.cabal-version }}
with:
path: ${{ steps.setup.outputs.cabal-store }}
key: ${{ env.key }}-plan-${{ hashFiles('**/plan.json') }}
restore-keys: ${{ env.key }}-
- name: Install dependencies
# If we had an exact cache hit, the dependencies will be up to date.
if: steps.cache.outputs.cache-hit != 'true'
run: cabal build all --only-dependencies
# Cache dependencies already here, so that we do not have to rebuild them should the subsequent steps fail.
- name: Save cached dependencies
uses: actions/cache/save@v4
# If we had an exact cache hit, trying to save the cache would error because of key clash.
if: steps.cache.outputs.cache-hit != 'true'
with:
path: ${{ steps.setup.outputs.cabal-store }}
key: ${{ steps.cache.outputs.cache-primary-key }}
- name: Build
run: cabal build all
- if: ${{ matrix.os == 'ubuntu-latest' && matrix.ghc-version == '9.12'}}
name: doctests
run: cabal run doctests
- name: Check cabal file
run: cabal check
module Main where
import System.Environment (getArgs)
import Test.DocTest (mainFromCabal)
import Prelude (IO, (=<<))
main :: IO ()
main = mainFromCabal "{{{name}}}" =<< getArgs
cabal
docs have gotten very good of late, and these recommended stanzas should be read with those docs handy.
Stanzas are used like so:
library
import: ghc2024-stanza
import: ghc-options-stanza
GHC2024 is the future. For the past, these stanzas reproduce the GHC2024 extensions for ghc’s between 9.2 and 9.10.
common ghc2024-additions
default-extensions:
DataKinds
DerivingStrategies
DisambiguateRecordFields
ExplicitNamespaces
GADTs
LambdaCase
MonoLocalBinds
RoleAnnotations
common ghc2024-stanza
if impl(ghc >=9.10)
default-language:
GHC2024
else
import: ghc2024-additions
default-language:
GHC2021
The stanzas below faithfully extend GHC2024 back into the ghc-8 series:
common ghc2021-additions
default-extensions:
BangPatterns
BinaryLiterals
ConstrainedClassMethods
ConstraintKinds
DeriveDataTypeable
DeriveFoldable
DeriveFunctor
DeriveGeneric
DeriveLift
DeriveTraversable
DoAndIfThenElse
EmptyCase
EmptyDataDecls
EmptyDataDeriving
ExistentialQuantification
ExplicitForAll
FlexibleContexts
FlexibleInstances
ForeignFunctionInterface
GADTSyntax
GeneralisedNewtypeDeriving
HexFloatLiterals
ImplicitPrelude
InstanceSigs
KindSignatures
MonomorphismRestriction
MultiParamTypeClasses
NamedFieldPuns
NamedWildCards
NumericUnderscores
PatternGuards
PolyKinds
PostfixOperators
RankNTypes
RelaxedPolyRec
ScopedTypeVariables
StandaloneDeriving
StarIsType
TraditionalRecordSyntax
TupleSections
TypeApplications
TypeOperators
TypeSynonymInstances
if impl(ghc <9.2) && impl(ghc >=8.10)
default-extensions:
ImportQualifiedPost
StandaloneKindSignatures
common ghc2024-additions
default-extensions:
DataKinds
DerivingStrategies
DisambiguateRecordFields
ExplicitNamespaces
GADTs
LambdaCase
MonoLocalBinds
RoleAnnotations
common ghc2024-stanza
if impl(ghc >=9.10)
default-language:
GHC2024
elif impl(ghc >=9.2)
import: ghc2024-additions
default-language:
GHC2021
else
import: ghc2021-additions
import: ghc2024-additions
default-language:
Haskell2010
Best-practice ghc-options:
common ghc-options-stanza
ghc-options:
-Wall
-Wcompat
-Wincomplete-record-updates
-Wincomplete-uni-patterns
-Wredundant-constraints
Best-practice exe ghc-options:
common ghc-options-exe-stanza
ghc-options:
-fforce-recomp
-funbox-strict-fields
-rtsopts
-threaded
-with-rtsopts=-N
https://github.com/martijnbastiaan/doctest-parallel
test-suite doctests
import: ghc2021-stanza
main-is: doctests.hs
hs-source-dirs: test
build-depends:
, base >=4.14 && <5
, doctest-parallel >=0.3 && <0.4
ghc-options: -threaded
type: exitcode-stdio-1.0
readmes can be included as documentation within a cabal file like so:
extra-doc-files:
ChangeLog.md
readme.md