Setup

Before we start writing any code, let’s ensure our environment is setup properly.

1. Install the dependencies

The easiest way to build Haskell code and manage dependencies is by using Stack.

We’re also going to need Docker. This isn’t strictly necessary, but it’s going to make a few things a lot easier, so I will be using it rather extensively.

Finally, I’m going to demonstrate setting up your build pipeline on Gitlab. You can use other repositories and CI/CD tools, but you’ll have to adapt the knowledge in this tutorial yourself.

I will henceforth assume you’ve successfully installed stack, docker and git. You may need to install some additional dependencies along the way, but that’s rare for consumer systems.

2. Create the project

Let’s test you’ve installed stack correctly. Run this in a console:

stack new haskell-tutorial
cd haskell-tutorial

And run the project for the first time, this will build the project’s dependencies, including the compiler, and might take a while the first time.

stack run

3. Initialize the repository

Create a new repository on gitlab, then initialize your repository locally and make your first commit:

git init
git remote add origin <your repository URL>
git add .
git commit -m"initial commit"
git push -u origin master

4. Setup CI/CD pipeline

Because Haskell has such powerful static checks, it makes a lot of sense to have a CI pipeline even if you don’t do a thing with the results.

Create a .gitlab-ci.yml file and open it with your editor of choice.

A basic CI pipeline that caches our dependencies looks like this:

stages:
  - build

variables:
  DOCKER_DRIVER: overlay2
  STACK_ROOT: ${CI_PROJECT_DIR}/.stack

build-backend:
  stage: build
  image: haskell:latest
  cache:
    paths:
      - ${STACK_ROOT}
      - .stack-work
  script:
    - stack
      --stack-root ${STACK_ROOT}
      build

You could push this configuration to Gitlab and it will build your project. The first run will take a while as stack downloads and install ghc, the compiler.

4.1 Basing on the alpine image (optional)

You might (like me) not like using the haskell image, and rather roll your own, based on alpine. This is a bit more involved and only for those already familiar with gitlab and docker.

First, we’re going to make our own container image for building our Haskell application. Let’s call it dockerfiles/buildenv.

FROM alpine:latest

ENV PATH ${PATH}:/root/.cabal/bin:/root/.local/bin

RUN apk add --no-cache ghc curl musl-dev zlib-dev postgresql-dev
RUN curl -sSL https://get.haskellstack.org/ | sh

And let’s build and use it in our CI pipeline.

stages:
  - build-requirements
  - build

variables:
  DOCKER_DRIVER: overlay2
  BUILD_ENV_IMAGE_TAG: ${CI_REGISTRY_IMAGE}/buildenv
  BUILD_ENV_IMAGE_TAG_VERSIONED: ${BUILD_ENV_IMAGE_TAG}:${CI_COMMIT_SHA}
  STACK_ROOT: ${CI_PROJECT_DIR}/.stack

build-environment:
  stage: build-requirements
  when: manual
  image: docker:latest
  services:
    - docker:dind
  script:
    - docker login -u gitlab-ci-token -p ${CI_BUILD_TOKEN} registry.gitlab.com
    - docker build -f dockerfiles/buildenv -t ${BUILD_ENV_IMAGE_TAG} .
    - docker tag ${BUILD_ENV_IMAGE_TAG} ${BUILD_ENV_IMAGE_TAG_VERSIONED}
    #push twice, both latest and versioned tags
    - docker push ${BUILD_ENV_IMAGE_TAG}
    - docker push ${BUILD_ENV_IMAGE_TAG_VERSIONED}

build-backend:
  stage: build
  image: ${BUILD_ENV_IMAGE_TAG}:latest
  cache:
    paths:
      - ${STACK_ROOT}
      - .stack-work
  script:
    - stack
      --stack-root ${STACK_ROOT}
      build
      --system-ghc

It is quite likely you’ve now run into an error that looks something like this:

No setup information found for ghc-8.6.5 on your platform.
This probably means a GHC bindist has not yet been added for OS key 'linux64-ncurses6'.
Supported versions: ghc-7.10.3, ghc-8.0.1, ghc-8.0.2, ghc-8.2.1, ghc-8.2.2

This is because the file stack.yaml specifies which resolver (the list of stack-curated dependencies) to use, which in turn determines the version of ghc we require, and that version just has not been ported to alpine yet. However, the versions mentioned in the error message are not the ones we can use, as we’re using –system-ghc, the right version should be listed in the logs of the CI job creating your build environment. In my case:

(25/37) Installing ncurses-terminfo-base (6.1_p20190518-r0)
(26/37) Installing ncurses-terminfo (6.1_p20190518-r0)
(27/37) Installing ncurses-libs (6.1_p20190518-r0)
(28/37) Installing ghc (8.4.3-r0)

I will need to speficy a resolver using ghc-8.4.3. So, in stack.yaml, I changed resolver: lts-13.27 to resolver: ghc-8.4.3. You can find a list of resolvers at https://www.stackage.org/

5. Ligatures

I’m not going to cover IDEs, as that is a hotly debated topic that I have no interest getting into. However, I’ve found that using ligatures helps a lot with code readability in Haskell, so I would recommend using an IDE that supports them.

overview
next