How I (currently) start a Haskell project in NixOS

Tagged with: haskell nixos

Learning Haskell over a long period, due to extremely limited free time, has had one interesting side effect. In the years I've been investing an hour here and an hour there into learning the Haskell way, the Haskell way has itself changed numerous times. So when I started what will hopefully become my first non-trivial, non-framework-assisted Haskell program recently I wasn't too surprised that I didn't yet know the right way to start.

Starting without stack

Stack is what prompted my first successful attempt to write any Haskell at all. Before it was released I struggled just to get a running compiler. It's a good tool and I suggest you use it when you're starting with the language.

But along the way I started running my home laptop on NixOS. I enjoy NixOS because I can recreate my system from a plain text configuration whenever I get a new laptop, but I've had problems getting Stack projects running on it. Since I spend very little time learning Haskell and I wanted to get a project started rather than debugging infrastructure, Stack had to go.

Plain old Cabal and Nix

If you want to start a Haskell project in Nix you'll eventually find your way to Gabriel Gonzalez's monster guide to the subject. What follows is not an update, replacement or amendment to that. It's only my distillation of the quick steps I found to get a new project started in the way I like them. I'm assuming you already have Cabal the tool installed (nix-env -iA nixos.cabal-install) and cabal2nix (nix-env -iA nixos.cabal2nix).

First I run cabal init in the directory in which I want my code to live. The app I'm starting will be both an executable and a library so I answer cabal's questions accordingly. I like to write my code into a library and then have an executable being just one user of that library so the library isn't too tied to my implementation. To that end I create an app directory and move Main.hs in there. I also create a test directory and touch test/Spec.hs. Finally I create a nix directory to store the project's nix configurations.

Before creating any nix configuration it's wise to edit the project cabal file a little. I expose any modules I expect to create in the library section, include expected build depends, and make projectName a build dep for app and test. The test part you'll likely need to add. It might look a little like the below.

test-suite projectName-test
  type:                exitcode-stdio-1.0
  hs-source-dirs:      test
  main-is:             Spec.hs
  build-depends:       base
                     , projectName
                     , hspec
                     , QuickCheck
  ghc-options:         -threaded -rtsopts -with-rtsopts=-N
  default-language:    Haskell2010

Now we can write our nix configurations. First get cabal2nix to create your default.nix with cabal2nix . > nix/default.nix. You'll need to edit what it produces to point the src directory up one level since it normally expects to be at the same level, which I avoid because I think looks a bit untidy. There's likely a way to automate that but I haven't looked for one.

Now add a nix/projectName.nix file that looks like the below:

let

pkgs = import <nixpkgs> { };

in
{ projectName = pkgs.haskellPackages.callPackage ./default.nix { };
}

This should leave a structure like:

parent_directory/
       projectName.cabal
       ...
 app/
  Main.hs
       nix/
           default.nix
           projectName.nix
 src/
  ProjectLib.hs
 test/
  Spec.hs

The first build

Build the project in Nix with nix-build nix/projectName.nix. Now you can enter a fully set up command line environment with nix-shell --attr projectName.env nix/projectName.nix. Once inside that shell run cabal new-build any time you want to rebuild your project. Running emacs from the nix-shell will also ensure your emacs haskell-mode is using the right environment.

Start writing a Spec

Import your libraries and write an initial expected behviour. Remember if you define your own data types then they need to be exported in the lib along with their constructors in order to be usable in the test. For my nascent chess program my first spec looked like this:

{-# LANGUAGE OverloadedStrings #-}

module Main where

import ChessLib
import Test.Hspec

main :: IO ()
main = hspec $
  describe "newGame" $
    it "sets up a Position in the tradition chess starting position" $
      newGame `shouldBe` Start "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1"

After watching that fail appropriately I'm ready to start the red, green, refactor cycle. Once the spec is passing I make my first commit, write my next spec and, as far as I'm concerned, the project is officially underway.