Quick and minimal Haskell hacking with Nix

Posted on September 6, 2017

This post will not explain in detail how Nix can build haskell projects or set up development environments, but simply show one workflow that I use a lot. Please refer to this manual, Gabriel Gonzalez’s guide and various blog posts out there to find out more about Nix and Haskell.

Alright, so I often end up wanting to quickly experiment with some ideas in a ghci session or a standalone Haskell module. Sometimes I can get away by simply prototyping my idea with data types and functions from base, but sometimes (most of the time, really) I want (or need) to use other libraries. I’d like to have a magic command to which I would give a ghc version and a list of haskell packages, and I would end up with an environment with ghc/ghci and all the aforementionned packages pre-installed in that ghc’s package database, so that just running ghci would be enough for being able to use those packages. Similarly, standalone modules could be built just with ghc --make for example. No need for a cabal file, cabal-install or stack.

Well, this is possible with Nix. The nix-shell program allows us to enter some environment that possibly requires downloading, building and running several things. The precise provisioning process depends a lot on what the environment consists in. It turns out that the Haskell infrastructure in Nix provides some functions out of the box for describing environments that consist in a ghc install along with some Haskell packages from Hackage. Making use of them is as simple as:

$ nix-shell -p "haskell.packages.ghc821.ghcWithPackages (pkgs: [pkgs.aeson pkgs.dlist])"

This enters a shell with GHC 8.2.1 with aeson and dlist (and their transitive dependencies) installed in its package database. You might even fetch all those things from a binary cache with a little bit of luck!

The text between double quotes is a function call, in the Nix programming language. It calls a function, haskell.packages.ghc821.ghcWithPackages (. here is simply field/attribute access, like in many OO languages), provided by the standard Haskell infrastructure in nixpkgs, with an argument that’s itself a function: pkgs: [pkgs.aeson pkgs.dlist] is equivalent to (pseudo Haskell code) \pkgs -> [aeson pkgs, dlist pkgs] where aeson and dlist would be fields of a big record containing all Haskell packages. So this ghcWithPackages function lets us provision a ghc package database using a package set that it is providing us with, pkgs.

Note: a haskell package set in nix is basically any record that provides the packages you need, where packages must be declared under a very precise shape. The one we’re using here (haskell.packages.ghc821) is derived from the latest stackage LTS (9.x) but we could very well be calling thebestpackageset.ghcWithPackages instead, provided that we define thebestpackageset somewhere and that its definition is valid. For example, you could simply extend the latest LTS with a few packages of yours that nix should get from github, by simply using the record extension syntax of Nix. Or you could override just a few package versions from the LTS. Or you could even put together an entire package set yourself.

In that new shell, you can verify that you indeed get the GHC you asked for and that you can use the said packages:

[nix-shell:~]$ ghc --version
The Glorious Glasgow Haskell Compilation System, version 8.2.1

[nix-shell:~]$ ghc-pkg list
/nix/store/wrrw9c2dsw3d4vmfck7dfx3br33c6pd1-ghc-8.2.1-with-packages/lib/ghc-8.2.1/package.conf.d
    Cabal-2.0.0.2
    aeson-1.1.2.0
    array-0.5.2.0
    attoparsec-0.13.1.0
    base-4.10.0.0
    base-compat-0.9.3
    binary-0.8.5.1
    bytestring-0.10.8.2
    containers-0.5.10.2
    deepseq-1.4.3.0
    directory-1.3.0.2
    dlist-0.8.0.3
    filepath-1.4.1.2
    ghc-8.2.1
    ghc-boot-8.2.1
    ghc-boot-th-8.2.1
    ghc-compact-0.1.0.0
    ghc-prim-0.5.1.0
    ghci-8.2.1
    hashable-1.2.6.1
    haskeline-0.7.4.0
    hoopl-3.10.2.2
    hpc-0.6.0.3
    integer-gmp-1.0.1.0
    integer-logarithms-1.0.2
    old-locale-1.0.0.7
    pretty-1.1.3.3
    primitive-0.6.2.0
    process-1.6.1.0
    random-1.1
    rts-1.0
    scientific-0.3.5.1
    tagged-0.8.5
    template-haskell-2.12.0.0
    terminfo-0.4.1.0
    text-1.2.2.2
    time-1.8.0.2
    time-locale-compat-0.1.1.3
    transformers-0.5.2.0
    transformers-compat-0.5.1.4
    unix-2.7.2.2
    unordered-containers-0.2.8.0
    uuid-types-1.0.3
    vector-0.12.0.1
    xhtml-3000.2.2

[nix-shell:~]$ ghci
GHCi, version 8.2.1: http://www.haskell.org/ghc/  :? for help
Prelude> import Data.Aeson
Prelude Data.Aeson> import Data.DList
Prelude Data.Aeson Data.DList>

Finally, you can define a very handy shell function for making it easier to spin up a new shell without having to remember the precise syntax for the nix-shell call above. Add this to your ~/.bash_profile, ~/.bashrc or what have you:

function nix-haskell() {
	if [[ $# -lt 2 ]];
	then
		echo "Must provide a ghc version (e.g ghc821) and at least one package"
		return 1;
	else
		ghcver=$1
		pkgs=${@:2}
		echo "Starting haskell shell, ghc = $ghcver, pkgs = $pkgs"
		nix-shell -p "haskell.packages.$ghcver.ghcWithPackages (pkgs: with pkgs; [$pkgs])"
	fi
}

The with pkgs; bit simply allows us to say [aeson dlist] instead of [pkgs.aeson pkgs.dlist], it just “opens” the pkgs “namespace”, like RecordWildCards in Haskell, when you write SomeRecord{..} to bring all the fields of a record in scope, with the field selector name as variable name for each field.

Now entering our previous shell is even simpler!

$ nix-haskell ghc821 aeson dlist

Of course, if you need private dependencies or fancier environments, you do need to resort to writing a shell.nix that carefully puts everything together (not that it’s that complicated, but definitely not as simple as this nix-haskell command). However, for a quick hacking session or exploration of an idea, I’m not aware of a nicer solution.

Powered by Hakyll - RSS feed - servant paper