A Haskell [c]hecklist. For starting off, rebooting, or just freshening up.
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, cut/paste or
tangle
. - [ ] (optional) read through the cabal stanzas section, and cut/paste some stanzas. Or not.
- [ ] (required) hack happy Haskell for your new pet project.
- [ ] follow the checklist when you want to tidy up.
Welcome to Haskell!
- [ ] version bump
Check current hackage version and bump cabal file version.
- [ ] ghcup upgrades
Check if tooling is current, and upgrade.
ghcup list -c set -r
ghc 9.8.1 latest,base-4.19.0.0 hls-powered,2023-10-09 cabal 3.10.2.0 latest hls 2.5.0.0 latest stack 2.13.1 latest ghcup 0.1.20.0 latest,recommended
- [ ] cabal.project check
Is the cabal.project & cabal.project.local files clean?
cat cabal.project
- [ ] upstream publishings
Have upstream dependencies been published on Hackage?
- [ ] cabal update
cabal update
- [ ] cabal gen-bounds
cabal gen-bounds
- [ ] cabal build –ghc-options=-Wunused-packages
cabal clean && cabal build --ghc-options=-Wunused-packages
- [ ] cabal-fix
cabal-fix check
cabal-fix inplace
- [ ] cabal build –prefer-oldest
cabal build --prefer-oldest
- [ ] FIXMEs & TODOs
- [ ] pragma cleanup
- [ ] doctests
cabal-docspec
https://github.com/martijnbastiaan/doctest-parallel
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
- [ ] exact version bump
- [ ] branch, push & check CI
- [ ] haddock
cabal haddock
- [ ] readme
- [ ] ChangeLog
- [ ] 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=GHC2021 --license=BSD-2-Clause -p minimal
[Log] Using cabal specification: 3.0 [Log] Creating fresh file LICENSE... [Log] Creating fresh file CHANGELOG.md... [Log] Creating fresh directory ./src... [Log] Creating fresh file src/MyLib.hs... [Log] Creating fresh directory ./test... [Log] Creating fresh file test/Main.hs... [Log] Creating fresh file minimal.cabal... [Warning] 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:
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-fix --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.
On emacs, inserting appropriate value in the macros below, adding this file to the project directory and running org-babel-tangle
will add files directly.
Macro Replacement (The Org Manual)
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}}}
===
[![Hackage](https://img.shields.io/hackage/v/{{{name}}}.svg)](https://hackage.haskell.org/package/{{{name}}})
[![Build Status](https://github.com/{{{github-username}}}/{{{name}}}/workflows/haskell-ci/badge.svg)](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
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@v16
build:
name: GHC ${{ matrix.ghc-version }} on ${{ matrix.os }}
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest]
ghc-version: ['9.10', '9.8', '9.6']
include:
- os: windows-latest
ghc-version: '9.8'
- os: macos-latest
ghc-version: '9.8'
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.8'}}
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: ghc2021-stanza
import: ghc-options-stanza
GHC2021 is the future. For the past, this stanza reproduces the GHC2021 extensions for ghc’s prior to 9.2.
common ghc2021-stanza
if impl(ghc >=9.2)
default-language:
GHC2021
if impl(ghc <9.2)
default-language:
Haskell2010
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
-- but keeping ormolu happy
if impl(ghc >=8.10)
default-extensions:
NoImportQualifiedPost
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
readme.org comes out scrambled eggs, but one day it might not:
extra-doc-files:
ChangeLog.md
readme.org
It’s kind of a shame that usage of emacs has declined amongst the Haskell community of late. Emacs praxis is radically shifting, partially due to the introduction of treesitter and the rewrites needed. haskell-ng-mode has 500 lines of elisp versus the 27000 line monster that is haskell-mode. Haskell could learn a thing or two about how old projects can undergo paradigm shift.
See my doom emacs dotfiles for the boring details.
Despite its corniness and fragility, org-mode is now integral to my development loop.
- Using org-mode is particularly helpful where rebooting ghci requires a large amount of state. A complex function, say, with intermediate results can be laid out using org-mode and state-of-debugging sessions can evolve and be remembered between sessions.
- It works well as an alternative readme, with no gap between code blocks as basic tests and code blocks as usage documentation.
- Org-mode provides a
curation
of historical ghci work, in between the complete backlog of computations, and a polished up module. - it enables a form of parallel type-tetris that can’t be had with any repl.
- Note taking can be wider, and encompass shell commands, copy/pasted code snippets, sites visited, random thoughts and unexplored byways.
For haskell-ng-mode, it requires the ob-haskell-ng package.
- added doctest-parallel to checklist, docs & CI
- removed cabal-docspecs from CI
This is version 2 of the checklist, with a substantial diff to version 1. The initial Haskell [c]hecklist was released around ghc-8.10, and, at time of writing, ghc-9.8.1 is in `ghcup list`.
The checklist now concentrates on a cabal-style workflow. I personally no longer use stack and would be concerned that any stack-based advice would become stale. Stack is also, in my opinion, a complete workflow compared with cabal where gaps remain.
The use of templates has been abandoned in favour of cabal init
, with advice and snippets around additions.
The combination of emacs org-mode and Haskell development has progressed, and for even more bespokity, I am experimenting with haskell-ng. Developments surrounding cabal are in a state of flux, and, until stability, I use cabal-fix for my cabal file needs.
Or, as Othello quips, “welcome, sir, to Cyprus. Goats and Monkeys!” Haskell is this corner solution to several problem domains difficult to pin, existing beyond some line demarcating the civilised empires of software development. Much of it will not make sense at first, and maybe ever, but if you stay long enough, you’ll begin to grow fond of even the goats and monkeys.
Birthed by committee in 1987 for use as an academic tool, it has now grown to not only be the 28th most popular language for tutorials, but also used industrially by over 0.2% of github users, making it somewhere between the 25th and 50th most popular language on the planet. Even before deep subsumption queer-coded the place, the community has been diverse, with both American and European programmers in its ranks.
At 37, you can’t expect Haskell to have tight onboarding, or clean lines. Getting the dad bod in shape, the wine-mom belly some room, knocking off a few of the rougher edges can be painful. Almost uniquely, though, the Haskell project seems dedicated to doing this, and time and again makes difficult decisions and takes risks that our corporate cousins would never take in the dark forest of software design.
So, at times, old stalwarts drag themselves away from their rust, their OCaml and Idris Two, and start yelling about stuff, waking up even older, white male professors via their mailing-lists, and they whine, on what used to be Twitter, about how their tutorials need editing. Again. Or someone announces Haskell is dying, or dead already, or has bad tooling, and that some committee somewhere must act, or has already, irrevocably, acted in poor taste. Popcorn gets thrown, hands are wrung, and then it all settles down again; we all just resume whatever we were doing in whatever corners we play in before the bru-ha-ha begun.
How it all works, how the work gets done, who is in charge, where is it all going; these are questions we don’t care to look at too closely. At 37, sometimes a vibe is all you have left.
So here’s what you need to know, as you start your Haskell journey:
- tooling is great, and getting to be first-in-class. Complainants usually have old setups they’re trying to freshen up, haven’t read the manual, or are grinding axes.
- documentation is getting better, but used to be poor, and docs can be difficult to backport. Most internet advice is poorly curated by search algorithms and not current.
- the secret sauce of Haskell is the language pragmas. Innovation gets wrapped up in new pragmas that the user can choose to turn on. GHC2021 is an important milestone.
- you probably wont get a job in Haskell. Do it for love.
- fancy Haskell is over-rated, and unfancy (pattern matching, composition-style, type-first coding, ghci) is a joy.
- you will enjoy coding in Haskell, to the point where it becomes painful to code in anything else.
- the code you write will be the best you ever write, and it will survive (subject to staying current with the GHC grind)
- you will be disappointed with the number of bums actually on seats. Dependency management is very important - before you commit to any dependency, look at the upstream chain for signs of care and attention.
- GHC is a monopolist provider of compilation support.
- Haskell has never been corporatised. We’ve had our suger mommas, yes, but we are no indentured slave to some global capitalist machine, oi!
- Learn to love strings. Compilation is strings all the way down. No matter how you dress them up, it’s all strings in a long computation chain.
Haskell, is above all, lovable. Well-crafted, solid, unfancy, machine precisioned where it matters, sludgy and open to ideas where it doesn’t.
Enjoy your time with us!