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 in your computer. This is because Nix can run shells with access to programs that are only temporarily installed in 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 it will use those to setup 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
];
}