How I (currently) start a Haskell project in 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.
-suite projectName-test
testtype: exitcode-stdio-1.0
-source-dirs: test
hs-is: Spec.hs
main-depends: base
build
, projectName
, hspecQuickCheck
, -options: -threaded -rtsopts -with-rtsopts=-N
ghc-language: Haskell2010 default
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 ()
= hspec $
main "newGame" $
describe "sets up a Position in the tradition chess starting position" $
it `shouldBe` Start "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1" newGame
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.