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.
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: Haskell2010Now 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.hsThe 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.