Home

Haskell Project Checklist

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

---
resolver: lts-9.18
packages:
  - '.'
extra-deps: []

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 later

2. Run Everything Through make

Makefile

all: setup build test lint

.PHONY: setup
setup:
	stack setup
	stack build --dependencies-only --test --no-run-tests
	stack install hlint weeder

.PHONY: build
build:
	stack build --pedantic --test --no-run-tests

.PHONY: test
test:
	stack test

.PHONY: lint
lint:
	hlint .
	weeder .

3. Use Hspec

package.yaml

tests:
  spec:
    main: Spec.hs
    source-dirs: test
    dependencies:
      - {package-name}
      - hspec

test/Spec.hs

{-# OPTIONS_GHC -F -pgmF hspec-discover #-}

Add modules that export a spec :: Spec function and match test/**/*Spec.hs.

4. Use Doctest

package.yaml

tests:
  spec:
    # ...

  doctest:
    main: DocTest.hs
    source-dirs: .
    dependencies:
      - doctest
      - Glob

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 ++ paths

Fill 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

---
- ignore:
    name: Redundant do
    within: spec

.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: native

The 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

Haskell CI Example 

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 --test

Sometimes I have this run on CI, sometimes I don’t.

16 Dec 2017, tagged with haskell