Rewriting to Haskell–Project Setup

Posted on March 3, 2020 by Riccardo

Rewriting to Haskell (Series)

Rewriting to Haskell–IntroRewriting to Haskell–Project SetupRewriting to Haskell–DeploymentRewriting to Haskell–Automatic FormattingRewriting to Haskell–ConfigurationRewriting to Haskell–Standing on the shoulders of RailsRewriting to Haskell–Making GHC More NitpickyRewriting to Haskell–TestingRewriting to Haskell–LintingRewriting to Haskell–Parsing Query ParamsRewriting to Haskell–Parsing Query Params, AgainRewriting to Haskell–Errors

We decided to go with Servant to rewrite the current Rails API for Stream. We don't really have a strong reason for that. We just like it and we believe it will allow us to do some cool stuff down the line!

Also, we picked Stack because it's what we are used to and seems to be less cryptic than Cabal.

We created the project with the following command:

stack new haskell servant
#     ^ Create a new project..
#         ^ ..in a new haskell/ folder..
#                 ^ ..using the Servant template.

The Servant template creates a dummy application with an hardcoded endpoint and some tests that allow to start playing with code right away.

CI

Stream is already using CircleCI, so we opened the Haskell languge guide and started copy / pasting like there was no tomorrow.

We ended up with the following config.yml file that supports Rails, Elm and Haskell. See inlined comments for more info.

---
version: 2.1

commands:
  install_dependencies:
# ^ Define a reusable command (see invokations below) to install the dependencies needed for Rails and Elm.
    steps:
      - restore_cache:
          name: Restore bundle cache
          key: stream-{{ checksum "Gemfile.lock" }}

      - restore_cache:
          name: Restore yarn cache
          key: stream-yarn-{{ checksum "yarn.lock" }}

      - run: bundle install --path vendor/bundle

      - run: yarn install

      - save_cache:
          name: Store bundle cache
          key: stream-{{ checksum "Gemfile.lock" }}
          paths:
            - vendor/bundle

      - save_cache:
          name: Store yarn cache
          key: stream-yarn-{{ checksum "yarn.lock" }}
          paths:
            - ~/.cache/yarn/v1
            - node_modules

jobs:
  build:
# ^ Test Rails and Elm code.

    working_directory: ~/stream
    docker:
      - image: circleci/ruby:2.4.1-node-browsers
        environment:
          PGHOST: 127.0.0.1
          PGUSER: stream
          RAILS_ENV: test
      - image: circleci/postgres:9.5-alpine
        environment:
          POSTGRES_USER: stream
          POSTGRES_DB: stream_test
          POSTGRES_PASSWORD: ""

    steps:
      - checkout

      - install_dependencies

      # sysconfcpus is a hack for elm-make
      # https://github.com/elm-lang/elm-compiler/issues/1473
      - run: |
          git clone https://github.com/obmarg/libsysconfcpus.git ~/libsysconfcpus
          cd ~/libsysconfcpus
          ./configure
          make
          sudo make install
          cd -

      - run: yarn test

      - run: yarn run elm-format --validate app/javascript/

      - run: sysconfcpus --num 2 yarn run elm-test app/javascript/tests

      - run: dockerize -wait tcp://localhost:5432 -timeout 1m

      - run: cp config/database.yml.example config/database.yml

      - run: bin/rails db:setup

      - run: sysconfcpus --num 2 bin/rspec spec

      - run: bundle exec bundle audit check --update

      - run: bundle exec brakeman --ensure-latest -A -5

      - run: bundle exec bundle outdated || true

  build_haskell:
# ^ Test Haskell code.

    working_directory: ~/stream/haskell

    docker:
      - image: fpco/stack-build:lts

    steps:
      - checkout:
          path: ~/stream

      - restore_cache:
          name: Restore Cached Dependencies
          keys:
            - stream-haskell-{{ checksum "stack.yaml" }}-{{ checksum "package.yaml" }}
            - stream-haskell-{{ checksum "stack.yaml" }}

      - run:
          name: Resolve/Update Dependencies
          command: stack --no-terminal setup

      - run:
          name: Run tests
          command: stack --no-terminal test

      - run:
          name: Install executable
          command: stack --no-terminal install

      - save_cache:
          name: Cache Dependencies
          key: stream-haskell-{{ checksum "stack.yaml" }}-{{ checksum "package.yaml" }}
          paths:
            - "/root/.stack"
            - ".stack-work"

      - store_artifacts:
          path: ~/.local/bin/haskell-exe
          destination: haskell-exe

  deploy:
# ^ Deploy the Rails and Elm application. We will see in a later post how to deploy the Servant application.

    working_directory: ~/stream

    docker:
      - image: circleci/ruby:2.4.1-node-browsers

    steps:
      - checkout

      - install_dependencies

      - run : |
            if [ "${CIRCLE_BRANCH}" == "master" ]; then
              bundle exec cap staging deploy
            elif [ "${CIRCLE_BRANCH}" == "production" ]; then
              bundle exec cap production deploy
            else
              echo "${CIRCLE_BRANCH} is a feature branch so no deploy"
            fi

workflows:
  version: 2
  build_and_deploy:
    jobs:
      - build
      - build_haskell
      - deploy:
          requires:
            - build
            - build_haskell

PinkLetter

It's one of the selected few I follow every week – Mateusz

Tired of RELEARNING webdev stuff?

  • A 100+ page book with the best links I curated over the years
  • An email once a week full of timeless software wisdom
  • Your recommended weekly dose of pink
  • Try before you buy? Check the archives.