My setup for messing about with Haskell scripts, 2021 edition

The problem

I want to open standalone .hs files in Emacs, do C-c C-l, and for things to Just Work.


A long time ago, it was easy. You could cabal install packages and they would be available globally. Over time you would amass a curated collection of packages, turning your laptop into a powerful Haskell scripting machine.

At some point, everything got broken. New Cabal uses a Nix-style database and nothing is visible by default. stack install should work, but somehow doesn't anyway.

In general, I'm always a bit saddened by the general trend in Haskell tooling (both cabal and stack) to seem to require projects — it all seems to discourage fly-by-night experimentation in favor of Serious Daytime Work on Software That Does Things. Of course, SDWoSTDT is what makes the world go round, but we can't all be serious all the time.

— Richard Eisenberg, Nov 3, 2018

There are Cabal scripts, where you can specify dependencies in the file itself and Cabal would download them automatically, but they don't work with cabal repl.

(As a side-note, Cabal and Stack are both great demonstrations that just a few flies are very effective at spoiling the ointment — except that in Stack's case the flies are bugs and in Cabal's case the flies are randomly missing features. Maybe the Haskell Foundation can change something here.)

I arrived at a solution with Nix, which is not super pretty — but at least it works and my REPL also works. As a bonus, I don't have to build many packages, since they are available in the binary cache — I can just download the prebuilt versions.

This is what I plan to use in 2021.

The solution with Nix

If you haven't yet, install Nix. See nix.dev for how to do it.

Let's say you have a folder with scripts. Let's also say are lucky and they don't require incompatible versions of dependencies.

You can create the following file in that folder, shell.nix:

# ./shell.nix

{ pkgs ? import <nixpkgs> { } }:

let
  haskellPackages = pkgs.haskellPackages.override {
    overrides = self: super:
      with pkgs.haskell.lib; {
        # Nothing here yet
      };
  };

  ghc = haskellPackages.ghcWithPackages (p:
    with p; [
      bytestring text containers unordered-containers aeson lens generic-lens
      # Whatever other packages you want — they will be available together
      # with all of their dependencies.
    ]);

in pkgs.mkShell { buildInputs = [ ghc pkgs.cabal-install ]; }

Then I add a shebang in every .hs script I have:

#! /usr/bin/env nix-shell
#! nix-shell -i runghc

module Main where ...

Finally, since I use Emacs, I create the following file for haskell-mode to pick up Nix automatically. Tweet at me (@availablegreen) if you can contribute a setup for other editors!

((haskell-mode . 
 ((haskell-process-type . ghci)
  (haskell-process-path-ghci . "nix-shell")
  (haskell-process-args-ghci . ("--command" "ghci")))))

Now I can do the following:

  • ./script.hs to run the script,
  • nix-shell --command "ghci script.hs" to enter REPL,
  • or just load the script in Emacs with haskell-mode.

I can also get different versions of packages if I need to, or load packages from GitHub, etc — see How to override dependency versions when building a Haskell project with Nix for more details on how to do it.

Addendum: Emacs + Nix

You will need this in your init.el for best results. Otherwise Emacs might complain about "SSL peer certificate or SSH remote key was not OK" when downloading new versions of dependencies.

;; Get path from shell.
(when (memq window-system '(mac ns x))
  (require 'exec-path-from-shell)
  (exec-path-from-shell-initialize)
  (exec-path-from-shell-copy-env "NIX_PATH")
  (exec-path-from-shell-copy-env "NIX_SSL_CERT_FILE"))