Haskell developer experience in NixOS

Posted in category nixos on 2017-06-05
Tags: haskell, nixos, vim

Table of Contents

Expectations

Not long passed since I first started using Nix and NixOS, that I realized that I would like to have my haskell development to be better integrated with it. I myself is a Vim user (even though I do try Emacs/Spacemacs from time to time) and as such not so much spoiled with lots and lots of tools available. But what I had I would like to be able to reliably use in my new OS.

Here is a very short list of tools that I need to be able to produce code in a more or less distruction free mode in Haskell with Vim:

Regardless of a specific OS I was using I always installed these packages globally. Such a global state quickly grows to be inconvenient. Every time I need another version of ghc-mod I have to re-install it thus breaking old setup. Same applies to some other packages too.

Nix-shell

nix-shell is a tool that starts an interactive shell based on a Nix expression. What is so good about this tool (not to say Nix package manager and a functional language, NixOS, etc.) is that it will bring all the build/runtime dependencies to live for this project to properly build and run. Nix package manager will also ensure that these packages will not be globally installed for the whole system (like it would be anywhere else), but only when I will need it. So, it sounds like what I need to make my Haskell projects build.

Let’s see how we can get going with literally any cabal-based Haskell project. First, we will need to come up with a Nix expression that will describe a project. Expression itself might look something like this:

{ nixpkgs ? import <nixpkgs> {}, compiler ? "default" }:

let

  inherit (nixpkgs) pkgs;

  f = { mkDerivation, amqp, base, network, stdenv, text }:
      mkDerivation {
        pname = "hs-amqpbus";
        version = "0.1.0.0";
        src = ./.;
        isLibrary = true;
        isExecutable = true;
        libraryHaskellDepends = [ amqp base network text ];
        executableHaskellDepends = [ base ];
        testHaskellDepends = [ base ];
        homepage = "https://github.com/kuznero/hs-amqpbus#readme";
        license = stdenv.lib.licenses.bsd3;
      };

  haskellPackages = if compiler == "default"
                       then pkgs.haskellPackages
                       else pkgs.haskell.packages.${compiler};

  drv = haskellPackages.callPackage f {};

in

  if pkgs.lib.inNixShell then drv.env else drv

I will not go into details of Nix language here. The only thing you should be able to notice rather soon is that this expression correlates with what is there in my cabal file. So, you can guess that it is using amqp, network and text packages, that its version is 0.1.0.0 and its name is hs-amqpbus.

NixOS developers came up with a very handy tool that will let us parse our cabal file and generate exact equivalent in Nix. It is called cabal2nix and here is how you might want to generate your expression:

cabal2nix --shell . > shell.nix

Basically, this will produce shell.nix file that is by default used by nix-shell. Now let’s start nix-shell. You can observe that it fetches and installs all required packages. Once the process completes you will be left inside a new bash shell where you can proceed with building and running your application:

cabal build
cabal run tool

nix-shell brings all necessary dependencies you might ever need to be able to develop, test and execute your application. Let’s now see how we can make sure that developer tools like ghc-mod, hindent, hlint are available as part of nix-shell. For that we will have to modify generated shell.nix slightly:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
{ nixpkgs ? import <nixpkgs> {}, compiler ? "default" }:

let

  inherit (nixpkgs) pkgs;

  f = { mkDerivation, amqp, base, network, stdenv, text, ghc-mod, hindent, hlint }:
      mkDerivation {
        pname = "hs-amqpbus";
        version = "0.1.0.0";
        src = ./.;
        isLibrary = true;
        isExecutable = true;
        libraryHaskellDepends = [ amqp base network text ];
        executableHaskellDepends = [ base ];
        testHaskellDepends = [ base ];
        buildDepends = [ ghc-mod hindent hlint ];
        homepage = "https://github.com/kuznero/hs-amqpbus#readme";
        license = stdenv.lib.licenses.bsd3;
      };

  haskellPackages = if compiler == "default"
                       then pkgs.haskellPackages
                       else pkgs.haskell.packages.${compiler};

  drv = haskellPackages.callPackage f {};

in

  if pkgs.lib.inNixShell then drv.env else drv

On line 7 notice additional arguments supplied to f function: ghc-mod, hindent and hlint. These arguments are then used in buildDepends attribute of our derivation (i.e. expression, describing our project) on line 17.

That is pretty much all, just exit your shell (if you happened to be in nix-shell already) and enter it again – this time you should notice more packages to be installed. Once process completes you can start your best friend - Vim (that hopefully has all you need to get it integrated with ghc-mod, etc.) and that should have access to all the build tools we specified earlier.

If something does not tick in your Vim, just make sure you have built your project with cabal build inside nix-shell.

Nix-build & Nix-env

Once the development process is done and you no longer need nix-shell you can build and install your project into Nix store. Here is how you can do this:

nix-build shell.nix

If build process completes successfully you should be able to find ./result symlink in the root of your project (that is a root that ensures that garbage collector in NixOS does not clean all dependencies of your project while you still need them). One last step is to be able to install a project that we just built:

nix-env -i ./result

That will register your project as installed in NixOS, and even if you will instruct nix manager to collect garbage, your new binaries will still be there available to you.

Great isolation and reproducibility

Nix is a great package manager with very simple idea - keeping side-effects under control. With Nix you can forget about DLL-hell! It is very much possible to work on the same system on several different projects that require incompatible set of dependencies. Nix’s system of tagging packages with SHA hashes ensures 100% reproducibility - in case any part of your package and/or its dependencies change, hash code changes, resulting in a completely different package.

There is way more to say about Nix and NixOS! One thing I would like to mention though is that Nix is the kind of technology that big enterprises call desruptive in a sense that they have to encompass it.