A boring Haskell script
Here at Type Classes, everything is written in Haskell and built with Nix. We haven’t written much about how we make that happen, but we aim to start putting more of our “boring Haskell” solutions to workaday problems out there. Here, I want to show you a small script that I used to generate package override files for a project I was building with
cabal2nix.The
cabal2nix package.
The Problem
When you build a Haskell project with Nix,For what we consider to be the best guide to using Nix for Haskell projects, please see this repository maintained by Gabriel Gonzalez. He discusses package overrides in the section about dependency management. it’s similar to building a project with
stack in that Nix relies on a curated set of Haskell packages that should all build together. However, if your project has dependencies that aren’t in that package set, Nix may not build your dependencies. In the case at hand, I was writing a CSV processing program using the
sv library.The
sv package. Unfortunately, when I added this dependency, Nix reported:It’s worth pointing out that they were marked as broken in the Nix package set I was using at the time, and I do not know if they are broken in all Nix package sets and I make no guarantees that you can reproduce this problem on your machine.
Package
svis marked as broken, refusing to evaluate.
I always appreciate how Nix says it is refusing to evaluate, rather than that it cannot evaluate. It might be able to, but it’s not going to try. But you can cajole Nix into evaluating it, or at least trying to evaluate it, by overriding the information in the Nix package set for the particular package you need.
I used
cabal2nix to generate the files. The basic process is that I would try to build my project, Nix would report that a dependency was marked as broken, and I would run a
cabal2nix command like this to generate the necessary file.
$ cabal2nix cabal://sv > sv.nix
What this is going to do is read the
.cabal file from the
sv package on Hackage (or, at least, from your local Hackage database – be sure to update your Cabal package database, kids!), convert the Cabal into Nix build instructions, and pipe that output to a file called
sv.nix.
Then I put the instructions for overriding the Nix package set into my
shell.nix file, so that the Nix environment (or “shell”) in which I will build and run my program has the build instructions for all the dependencies.
let pkgs = import <unstable> { }; haskellPackages = pkgs.haskellPackages.override { overrides = self: super: { sv = self.callPackage (import ./sv.nix) {}; }; }; tweets = haskellPackages.callPackage ./tweets.nix { }; in pkgs.mkShell { name = "shell"; buildInputs = tweets.env.nativeBuildInputs ++ [ pkgs.ghcid ]; }
This project also depends on the
time and
bytestring packages, but since they are not “marked as broken” in the Nix set of Haskell packages, I don’t need to say anything special about them.
However, it turns out that several packages
sv depends on were also marked as broken. So I went through this tedious process several times, generating a file for each of them and adding it to my
shell.nix file.
Then came the final straw: the test suites for each of these was also broken. That isn’t an uncommon situation with package overrides, since it often means you’re trying to build an older version of a library or something. There is a
--no-check flag for the
cabal2nix command that gives build instructions for the package without the test suite. I think you can manually add the
doCheck: false line to each of these files, but why would you do something manually and perhaps get it wrong? But, also, why would you keep running this command
cabal2nix cabal://sv --no-check > sv.nix
over and over again, when you could write a Haskell script and be done with it?
Time to automate
I can already hear you thinking, “A Haskell script? Surely you mean a Bash script.” But Haskell is what I write and what I know, and here at Type Classes, it’s what we reach for, even for the most mundane of tasks. So, I made a file called
updateOverrides.hs, and its entire contents are here:
Open with a shebang! We will use the
runhaskell interpreter.
#! /usr/bin/env runhaskell
A couple of imports. We want the
readProcess and
traverse_ functions from these modules of the
base package.
import System.Process import Data.Foldable
This is the list of the names of packages that need overrides.
packages :: [String] = packages "sv" [ "hw-balancedparens" , "hw-bits" , "hw-dsv" , "hw-rankselect" , "hw-simd" ] ,
The
update function calls an external process, reads its output, and writes that output to a file. In other words, it pipes the output of the
cabal2nix command to a file, as we were previously doing with
> in Bash. Notice the package name is concatenated to both the
cabal:// string and also the file name that it writes to.
update :: String -> IO () = update s "cabal2nix" ["cabal://" ++ s, "--no-check"] "" readProcess >>= writeFile (s ++ ".nix")
This is the action that
runhaskell will execute when we run the script.
= main traverse_ update packages
You might know about
fmap (or, if you prefer,
map over lists, and a list is what we have here), but have you heard the good news about
traverse and its lovely sibling,
traverse_? We want to apply the
update function to each one of the package names in the list, which we could do with a
map or
fmap function. But that would give us a return type of
[IO ()] and we don’t really want that. We want the end result to be an
IO effect, not a list. So,
traverse is an
fmap that also has the ability to “sequence” the type structure, or flip the layers around. Instead of a list of side-effects, we can traverse this list, accumulating the effects, and just “return” a side-effect, instead of a “list of effects.” And with
traverse_ we can even throw away the remaining “list” structure, because the underscore indicates that this function throws its result value away (while, of course, preserving the side effects).
To put it in more familiar terms,
traverse is what Haskellers reach for when you would, in many programming languages, reach for a
for loop. In fact, Haskell has a function called
for (and a sibling function,
for_)The
for functions. that is
traverse with its arguments flipped.
So, we could have written
mainIf I don’t mention that this doesn’t loop in the same way as loops in imperative languages and that recursion and looping are different and so forth, Lambda Man will come after me. So, I fulfill my obligations by saying
traverse is not a loop – but you can use it like one.
and that would have been fine! Note the rearrangement of the arguments. But
traverse_ is the more idiomatic way to “do a for loop” in Haskell.
Once our Nix environment was settled, the real fun could begin: