The goal of this article is to get you comfortable managing simple Haskell programs and projects using the Nix package manager without getting too much into the details. See haskell-template
if you want a ready-made Nix-based project template for Haskell.
Prerequisites
You are running either Linux or macOS 1 , and have installed the Nix package manager using these instructions 2 . You do not need to install anything else, including needing to install Haskell, as Nix will manage that for you.
Simple programs
Let us begin with the simplest Haskell program, try to compile and run it with the help of Nix.
-- HelloWorld.hs
module Main where
main :: IO ()
main = putStrLn "Hello World"
Haskell code is compiled by GHC, which is provided by the Nix package called “ghc”. How do we install it? According to the Nix manual this can be done by running the command nix-env -i ghc
. For Haskell developers, there is a better approach. Instead of installing packages in a global environment, you may install them to an isolated area and launch a shell with those packages in its environment. This is done using the nix-shell -p ghc
command.
# This drops us in a bash shell with ghc package installed and
# $PATH updated.
$ nix-shell -p ghc
...
# Now let's run our module.
[nix-shell:~] runhaskell HelloWorld.hs
Hello World
As you can see, nix-shell dropped us in a shell environment with the “ghc” package installed and activated. This puts runhaskell
(part of the “ghc” package) in your PATH, running which will compile and run your first Haskell program. When you exit the nix-shell (Ctrl+D
), runhaskell
will no longer be in scope, however the “ghc” package will have been cached so that subsequent invocations of nix-shell -p ghc
would not have to download and install it once again.
3
Using library dependencies
What if our program relied on an third-party Haskell library? The following program uses the brick UI library.
-- HelloWorld.hs
module Main where
import Brick
ui :: Widget ()
ui = str "Hello, world!"
main :: IO ()
main = simpleMain ui
We can no longer use the “ghc” package here. Fortunately, Nix is also a programming language, and as such as we can evaluate arbitrary Nix expressions to create a customized environment. Official Nix packages come from the nixpkgs channel, which provides a function called ghcWithPackages
. Evaluating this function, passing it a list of Haskell libraries (already in nixpkgs), will create an environment with both GHC and the specific Haskell libraries installed.
$ nix-shell \
-p "haskellPackages.ghcWithPackages (p: [p.brick])" \
--run "runhaskell HelloWorld.hs"
The --run
argument will invoke the given command instead of dropping us in an interactive shell. This single command does so much—install the Haskell compiler with the requested libraries, compile our program and run it!
Haskell scripts
You can use the above nix-shell command in the shebang to create self-contained Haskell scripts. Let us see an example, but using ghcid, instead of runhaskell:
#! /usr/bin/env -S"ANSWER=42" nix-shell
#! nix-shell -p ghcid
#! nix-shell -p "haskellPackages.ghcWithPackages (p: [p.shower])"
#! nix-shell -i "ghcid -c 'ghci -Wall' -T main"
import Shower (printer)
import System.Environment (getEnv)
main :: IO ()
main = do
let question = "The answer to life the universe and everything"
answer <- getEnv "ANSWER"
printer (question, "is", answer)
Save this file as myscript.hs
and run chmod u+x myscript.hs
to make it an executable, and then run it as ./myscript.hs
. Not only is it a self-sufficient script (depending on nothing but nix in the environment), but thanks to ghcid it also re-compiles and re-launches itself whenever it changes! See more examples here.
Cabal project
Haskell projects normally use cabal, and you might already be familiar with Stack which uses Cabal underneath. Nix is an alternative to Stack with many advantanges, chief of them being the creation of reproducible development environments using declarative configuration that handles even non-Haskell packages.
Adding Nix support to most Cabal projects is a matter of creating a file called default.nix
in the project root (just make sure you have a .cabal
file named appropriately). This file is by default used by commands like nix-build
and nix-shell
, which we will use when developing the project.
# default.nix
let
pkgs = import <nixpkgs> { };
in
pkgs.haskellPackages.developPackage {
root = ./.;
modifier = drv:
pkgs.haskell.lib.addBuildTools drv (with pkgs.haskellPackages;
[ cabal-install
ghcid
]);
}
Now if you run nix-shell
it will drop you in a shell with all Haskell dependencies (from .cabal file) installed. This will be your development shell; from here you can run your usual cabal
commands, and everything will function as expected.
$ nix-shell
...
[nix-shell:~] cabal new-build
..
If you only want to build the project, creating a final executable, use nix-build
.
Development dependencies
Notice the modifier
attribute in the previous example. It specifies a list of build dependencies, using the addBuildTools
function, that becomes available when we run either nix-shell
or nix-build
. Here, you will specify all the packages you need for development.
4
We speficied two—cabal
and ghcid
. If you removed cabal
from this list, then cabal will not be in scope of your nix-shell. We added ghcid
, which can be used to run a daemon that will recompile your project if any of the source files change; go ahead and give it a try using nix-shell --run ghcid
.
Overriding dependencies
The above will work as long as the libraries your project depends on exist on nixpkgs (which itself is derived from Stackage). That will not always be the case and you may want to override certain dependencies.
In Nix overriding library packages is rather straightforward. The aforementioned developPackage
function exposes this capability via the source-overrides
attribute. Suppose your cabal project depends on the named package at a particular git revision (e684a00
), then you would modify your default.nix
to look like:
let
pkgs = import <nixpkgs> { };
compilerVersion = "ghc865";
compiler = pkgs.haskell.packages."${compilerVersion}";
in
compiler.developPackage {
root = ./.;
source-overrides = {
named = builtins.fetchTarball
"https://github.com/monadfix/named/archive/e684a00.tar.gz";
};
modifier = drv:
pkgs.haskell.lib.addBuildTools drv (with pkgs.haskellPackages;
[ cabal-install
ghcid
]);
}
Now, if you re-run nix-shell
or nix-build
Nix will rebuild your package, and any packages depending on named
, using the new source.
Note that this example also demonstrates how to select a compiler version.
See Artyom’s tech notes for more on overriding Haskell dependencies in Nix.
Multi-package cabal project
developPackage
cannot be used if your project has multiple Haskell packages. You will have to go a few steps lower in the abstraction ladder, and use the underlying Nix functions (callCabal2nix
, shellFor
, extend
) in the default.nix
(or flake.nix
) of a multiple-package cabal project. See https://github.com/srid/haskell-multi-nix for a full example.
Caching
Nix has builtin support for caching. Packages from the nixpkgs channel are already cached in the official cache. If you want to provide caching for your own packages, you may use nix-serve (from NixOS) or Cachix (third-party service).
Continuous Integration
Setting up CI for a Haskell project that already uses Nix is rather simple. If you use Github and Cachix, the easiest way is to use the cachix Github Action. 5
External links
- Official Nix manual on using Haskell
- Unofficial developer guide to Nix
-
haskell-template
: A prebuilt Haskell project template using Nix flakes, among other defaults - Incrementally package a Haskell program using Nix
tree
package, so as to dispay the directory tree of the current directory, you would run: nix-shell -p tree --run tree
.
pkgs.lib.haskell.inNixShell
to conditionally include dependencies on nix-shell but not nix-build.