Nixify your haskell project: Introduction

Hey all, I would like to announce a blog post that I wrote for people who want to use Nix in their Haskell projects. It is first in the series of blog posts that also explains related development workflows (such as running services locally via Nix)

Why I wrote it?

When I first started with Haskell, my toolchain of choice was stack with GHCup and for most of the projects it would work just fine but it wasn’t always reproducible mostly because of either ~/.stack or ~/<project-path>/.stack-work getting corrupted. That’s when I was exploring alternatives and bumped into Nix.

At first I had a tough time, like any other Nixer out there, but soon I had a development environment that not just made the builds reproducible but also let me configure everything I will ever need to contribute to the project. For example, treefmt-nix to configure formatters, flake-parts for bringing NixOS like module system to Flakes, services-flake to run and configure service dependencies like Postgresql and much more.

I can’t cover all of this in one post and hence I have divided them up into a series of posts. The first post starts Nixifying a Haskell project from scratch, which anyone with a basic understanding of Nix and flakes can follow along. Considering this is my first blog post, it might still be a lot of iterations away from being beginner friendly. So, please feel free to provide any feedback!

Credits to @srid for the numerous feedback.

6 Likes

I’ve tried many time to read the flake doc, but I don’t understand any of it.
Could you explain the difference (and benefit) of nix flake and a nix shell (and also how it differs from stack nix integration).

TL;DR Flakes offer advantages in terms of clarity and purity, making them an attractive option for Nix-based development workflows.

The two primary benefits of flake that I see are:

  • A schema that clearly specifies the inputs and outputs of the flake.nix file.
  • Purity by default.

This comment from a different post does an excellent job of explaining how nix-shell has evolved over time.

how it differs from stack nix integration

Nix integration with Stack is essentially a wrapper around Nix, designed to provide a more familiar environment for Stack users without introducing a new language. Under the hood, it utilizes the nix-shell command to create a development shell with the necessary packages loaded.

So why not stick with it? Firstly, it eliminates the need for two configuration files (stack.yaml and flake.nix ) that essentially serve the same purpose—package management. Secondly, while a wrapper can be beneficial when your goal is solely to build your Haskell project, it becomes burdensome when you need to configure more complex services, like PostgreSQL.

So, if I understand well, nix develop puts you in a development environment (like nix shell) but a different one, because it takes an input ???. What is this input ? (I guess the ouput is the environment). What makes using an input the environment more pure or reproductible than a nix.shell ?

What is this input ?

Input can be a link that points to some source. For example, it can be a link to a Github repository or it could be a path on your local file system. See here to understand the input schema better.

What makes using an input the environment more pure

The fact that your input always comes from the exact path mentioned in your flake.nix rather than from a nix-channel , which was the case in pre-flake, can result in nixpkgs commits being different for each individual, potentially leading to varying behavior. While there are workarounds to make it function consistently, it’s important to note that this isn’t the default behavior.

1 Like

I guess that the “input , output” thing is a way of making flakes composable (you can chain them ).
But to be reproductible you still need to start with the same input, am I right ?
If so (apart from the composability), it is no different from nix shell.

In fact, you can easily pin a package in a shell.nix using builtins.fetchTarBall which makes it totally reproductible.

The best thing about stack integration is I don’t have to worry about nix at all. There is no risk for me forgot typing nix shell or nix develop. (Maybe stack nix integration should offer a flake option)

For information my shell.nix (for stack) starts with

let pkgs =  (import (builtins.fetchTarball {
             name = "fames";
             url = "https://github.com/nixos/nixpkgs/archive/${import ./.nixpkgs}.tar.gz";
             }) {});

where .nixpgs is can be pinned with `make pin_nix’

pin_nix: .nixpkgs

.PHONY: .nixpkgs
.nixpkgs:
	cat ~/.nix-defexpr/channels/nixpkgs/.git-revision | sed 's/.*/"&"/' > $@

Maybe I reinvented nix-flake without knowitg it ?

That sounds more like a workaround, doesn’t it? at least in my opinion the flake way of handling this seems more of a default behaviour that should’ve been the case from the get go.

Again, there’s nothing wrong in using the nix integration with stack if that fits your use case and it doesn’t require you to learn nix either. The main disadvantage is when you want to configure something which stack doesn’t give you a wrapper for and then you will have to worry about learning nix.

I’m sure flakes are great . I am just trying to understand how they work and all I get are buzzwords like “pure”, “reproductible”, “input” “output” (which apply to nix shell alread) etc without actually explaining what the actual workflow is.

With more people contributing content, hoping this situation to improve soon!

If by ‘nix shell’ you are referring to a pkgs.mkShell with cabal, ghc, libs, etc. provided - then all you are getting is a Haskell development environment.

Whereas, when you fully nixify your project (the recommended way of going about it is to write a flake.nix that provides not only devShells but also packages, via the use of cabal2nix/callCabal2nix) you get not only development environment, but also packages/executables for your Haskell projects built by Nix, which in turn can also build Docker images; not to mention being able to run services (postgresql, etc.) all via Nix. You can deploy the same packages to the production node if necessary without any separate build process.

Try running nix run github:srid/emanote and it will build and run the Emanote Haskell executable without needing any system libraries or dependencies (ghc, stack, …) pre-installed.

By nix shell I mean nix-shell which read a shell.nix file and start a shell with all the enviroment (from that shell.nix file). Is this not as fully nixified as with using flake ?

I understand the benefit of nix-flake vs non nix, but I’m struggling to see the difference with nix-shell which I have believed was to provide exactly

not only development environment, but also packages/executables for your Haskell projects built by Nix, which in turn can also build Docker images; not to mention being able to run services (postgresql, etc.)

Basically, the sales pitch for nix-flake is the one I used to read 10 years ago about nix in general and I am not versed in nix enough to understant how it improves it.

Having said that, nowadays I only use nix indirectly via stack nix integration (which run a nix-shell for me).

That uses pkgs.mkShell (or pkgs.haskellPackages.shellFor).

Correct (as I explained above).