Getting started with Haskell and Nix

How to setup your Haskell environment as a beginner without cabal or stack

So, you want to learn Haskell? Congratulations! You're on your way to becoming a much better programmer in no time. However, as every programmer knows, the hardest part about programming is setting up your environment, especially when faced with multiple options. This can be particularly challenging for beginners, and even experienced programmers that want to play with a new technology. In Haskell, you'll encounter three primary options for environment setup: cabal, stack, and Nix, and plenty of people arguing for and against each of these options. This guide is here to help you quickly get started with Nix.

About Nix

Here is a quick what and why on Nix, feel free to skip this section if you are ready to get started.

What is Nix

Here is how the Nix user manual describes Nix:

Nix is a purely functional package manager. This means that it treats packages like values in purely functional programming languages such as Haskell — they are built by functions that don’t have side-effects, and they never change after they have been built. Nix stores packages in the Nix store, usually the directory /nix/store, where each package has its own unique subdirectory such as

/nix/store/b6gvzjyb2pg0kjfwrjmg1vfhh54ad73z-firefox-33.1/

where b6gvzjyb2pg0… is a unique identifier for the package that captures all its dependencies (it’s a cryptographic hash of the package’s build dependency graph). This enables many powerful features.

Why Nix?

Why should you choose Nix? Well, for one, it allows you to experiment with new technologies without the hassle of installing them on your computer. With Nix, you can create separate environments for each of your programming needs, allowing you to run different versions of a program or library without interference. In fact, with Nix you can quickly experiment with both stack and cabal to see what they are all about without having to install them. What's more, Nix is not limited to just Haskell - you can use it to create environments with any Unix software. NixOS, for example, is an entire operating system built from the ground up using Nix. Instead of learning and installing a different package manager for each technology you use, learning Nix can save you time and effort, enabling you to focus on coding instead of managing installations and dependencies. With Nix, you can spend less time configuring and more time coding!

Getting started

First, install Nix, the instructions will be slightly different for each operating system.

Once Nix is installed, you can immediately start coding without having to install anything else on your computer. This is because Nix can run shells with access to programs that are only temporarily installed on your computer without interfering with your installation. Once you exit a Nix shell those programs are no longer accessible because they were never actually installed to begin with.

The very first step with a new language is to play around with it without any commitment. For Haskell, you just want to play around in the ghci repl and kick the tires. You use nix-shell for this.

Basic Nix shell

Here is how you get a basic Haskell environment to play with:

nix-shell -p ghc

This gives you access to the ghc compiler, the ghci interactive repl, and the runhaskell command to execute scripts. All without having to install ghc in your computer.

Basic Nix shell for Haskell and other programs

Let’s say you want access to ghc, the ghcid development tool, and nodejs all in the same shell:

nix-shell -p ghc ghcid nodejs

This is how you get multiple programs in the same shell from the command line.

Using Haskell packages

Let’s say you want to use Aeson and Lens, two popular Haskell libraries that are not included by default, here is how you can get a shell with those libraries available:

nix-shell -p “haskellPackages.ghcWithPackages ( p: [p.aeson p.lens])”

The syntax is slightly more verbose, and since the expression is not one word we need to wrap it with quotation marks. If you wanted to do Haskell packages and other programs we do the same as above, just adding the program names:

nix-shell -p “haskellPackages.ghcWithPackages ( p: [p.aeson p.lens])” ghcid nodejs

Writing self-contained executable scripts

At this point having to write all these command line options might be getting too tiresome, so for simple executable files we can use the "shebang" syntax to turn them into scripts. The shebang syntax is when we start the top of an executable with something like #! /usr/bin/env bash to tell the operating system to use bash to execute a file. We can use the same with nix-shell:

#! /usr/bin/env nix-shell
#! nix-shell -p "haskellPackages.ghcWithPackages ( p: [p.aeson p.lens])" 
#! nix-shell -p ghcid nodejs
#! nix-shell -i runhaskell

##  put your haskell code here

import qualified Data.Aeson as A

main = do
   let myJson = A.encode "my json"
   print myJson

this allows you to write scripts in Haskell and execute them as if they were shell scripts.

Saving your development configuration with shell.nix

Once you have played around with nix-shell and you started building some code, you have written some scripts, but now you want to do a full project with modules that are called by other modules etc. and you are getting tired of having to type all the command line options into nix-shell, you can save all these commands using a special file called shell.nix.

Let’s say you want to save the options for the prompt in the last section, create a file called shell.nix on the top level of your project, and write the following in there:

{pkgs ? import <nixpkgs> {}}: with pkgs;
let
  myHaskell = haskellPackages.ghcWithPackages myHaskellPackages;
  myHaskellPackages = packages: with packages; [
    aeson
    lens
    # put haskell packages here
  ];
in
mkShell {
  buildInputs = [
    myHaskell
    ghcid
    nodejs
    # put any other binaries we need here
  ];
}

Now whenever you are in that directory, if you type nix-shell without any parameters, nix-shell will use what you wrote in shell.nix to set up your development environment, and you are ready to develop in your project.

The nice thing about Nix is that it’s not only for Haskell, you can use it for other things your project might need. Here is an example of a shell.nix for a multi-language project

{pkgs ? import <nixpkgs> {}}: with pkgs;
let
  myHaskell = haskellPackages.ghcWithPackages myHaskellPackages;
  myJulia = julia-bin;
  myPython = python3.withPackages myPythonPackages;
  myNode = nodejs;
  myJava = jdk11;
  myHaskellPackages = packages: with packages; [
    wai
    servant
    # put haskell packages here
  ];
  myPythonPackages = packages: with packages; [
    pandas
    numpy
    jupyter
    matplotlib
    # put python packages here
  ]; 
in
mkShell {
  buildInputs = [
    myHaskell
    myJulia
    myPython
    myNode
    myJava
    # put any other binaries we need here
    ghcid
    python39Packages.conda
    git
    curl
    tmux
  ];
}

Have your computer load your environment automatically

This is cool, but programmers are lazy, why do we have to type nix-shell every time we go into the directory where our project lives? You can have the computer automatically go into your Nix environment whenever you cd into this directory if you use direnv and .envrc.

First, you need to install direnv, and since you already have Nix installed you can use nix-env to install direnv. This is the command to do it:

nix-env -iA nixpkgs.direnv

Now to hook direnv into your environment you need the following command somewhere in .bashrc or .zshrc, depending on what shell you use:

eval "$(direnv hook zsh)

Now that direnv is installed, you need to tell it that your project should use it, so create a file called .envrc and put this line in it

use_nix

Now that your environment is ready, direnv will load it automatically every time you go there, but the very first time it will ask you for permission, which you give it by typing

direnv allow

Now you are ready to maximize your laziness and let the computer give you a different environment for every different project every time you cd into it!

What next?

If you made it this far, that means you found Nix to be useful, so I would suggest learning more about it. Nix Pills is a good resource to learn more about Nix.

TLDR

First, install Nix. Now you can code using the nix-shell command and some options. Here are some examples of options you can use depending on your needs:

# basic no-frills shell to code in haskell
nix-shell -p haskellPackages.ghc

# shell with some haskell packages included
nix-shell -p "haskellPackages.ghcWithPackages (p: [p.base p.text p.lens p.aeson])"

# shell with packages and ghcid and node
nix-shell -p "haskellPackages.ghcWithPackages (p: [p.base p.text p.lens p.aeson])" ghcid nodejs

These allow you to call the ghci repl and start coding.

You can turn your project into a script using shebang options. Here is an example of an executable file using shebang options:

#! /usr/bin/env nix-shell
#! nix-shell -p "haskellPackages.ghcWithPackages ( p: [p.aeson p.lens])" 
#! nix-shell -p ghcid nodejs
#! nix-shell -i runhaskell

## put your haskell code here

import qualified Data.Aeson as A

main = do
   let myJson = A.encode "my json"
   print myJson

If you want to save your options in a more permanent file to develop in your project you can create the file shell.nix and put them there. You can use direnv and .envrc to have your computer load the shell when you cd into the directory. Here is an example of a shell.nix file:

{pkgs ? import <nixpkgs> {}}: with pkgs;
let
  myHaskell = haskellPackages.ghcWithPackages myHaskellPackages;
  myHaskellPackages = packages: with packages; [
    aeson
    lens
    # put haskell packages here
  ];
in
mkShell {
  buildInputs = [
    myHaskell
    ghcid
    nodejs
    # put any other binaries we need here
  ];
}