The following are all the things I want in place for a Haskell project. This is primarily a copy-paste-able reference for myself, but I’ve also tried to explain or generalize some things to make it useful for anyone first bootstrapping a Haskell project.
NOTE: if you were brought here after googling something like “how to Haskell on Circle 2.0”, you’ll just need the Makefile and .circleci/config.yml.
1. Use stack & hpack
.gitignore
*.cabal
.stack-work
stack.yaml
package.yaml
---
name: {package-name}
version: '0.0.0'
category:
synopsis: Short synopsis
description: >
Longer, wrapping description.
author:
maintainer:
github: {username}/{package-name}
license: MIT
ghc-options: -Wall
dependencies:
- base >=4.8.0 && <5 # GHC 7.10+
library:
source-dirs: src
dependencies:
- text # for example
tests:
# More on this later2. Run Everything Through make
Makefile
3. Use Hspec
package.yaml
test/Spec.hs
Add modules that export a spec :: Spec function and match test/**/*Spec.hs.
4. Use Doctest
package.yaml
DocTest.hs
NOTE: doctest-discover is supposed to do this, but unfortunately it’s broken.
module Main (main) where
import System.FilePath.Glob
import Test.DocTest
main :: IO ()
main = do
let options =
-- For example
[ "-XOverloadedStrings"
]
paths <- globDir1 (compile "**/*.hs") "src"
doctest $ options ++ pathsFill your Haddocks with executable examples.
-- | Strip whitespace from the end of a string
--
-- >>> stripEnd "foo "
-- "foo"
--
stripEnd :: String -> String
stripEnd = -- ...See the Doctest documentation for more details.
5. Always Be Linting
As you saw, we have a make lint target that uses HLint and Weeder. I also have my editor configured to run stylish-haskell on write.
.hlint.yaml
.stylish-haskell.yaml
WARNING: opinionated!
---
steps:
- simple_align:
cases: false
top_level_patterns: false
records: false
- imports:
align: none
list_align: after_alias
pad_module_names: false
long_list_align: new_line_multiline
empty_list_align: right_after
list_padding: 4
separate_lists: false
space_surround: false
- language_pragmas:
style: vertical
align: false
remove_redundant: true
- trailing_whitespace: {}
columns: 80
newline: nativeThe defaults for weeder are usually fine for me.
6. Use Circle 2.0
When you set up the project, make sure you say it’s Haskell via the Other option in the language select; maybe they’ll add better support in the future.
.circleci/config.yml
---
version: 2.0
jobs:
build:
docker:
- image: fpco/stack-build:lts-9.18
steps:
- checkout
- restore_cache:
keys:
- stack-{{ .Branch }}-{{ checksum "stack.yaml" }}
- stack-{{ .Branch }}
- stack-
- run:
name: Dependencies
command: make setup
- run:
name: Build
command: make build
- save_cache:
key: stack-{{ .Branch }}-{{ checksum "stack.yaml" }}
paths:
- ~/.stack
- ./.stack-work
- run:
name: Test
command: make test
- run:
name: Lint
command: make lint
Quite nice.
Don’t forget to enable “build forked Pull Requests” in Circle’s settings.
7. Release to Hackage
I wrap this up in my own hackage-release script, but here are the relevant actions:
stack build --pedantic --test
stack upload .
And it’s a good practice to tag releases:
git tag --sign --message "v$version" "v$version"
git push --follow-tags
8. Add to Stackage
Check the documentation here. In short, just open a Pull Request adding yourself and/or your package to build-constraints.yaml. It can be done without even leaving GitHub.
You should ensure your package builds “on nightly”. I add a target for this to my Makefile:
.PHONY: check-nightly
check-nightly:
stack setup --resolver nightly
stack build --resolver nightly --pedantic --testSometimes I have this run on CI, sometimes I don’t.