Using Nix for Haskell development in Emacs with LSP


NOTE (2022-06-16): I’ve switched to use Nix Flakes instead. See https://gitlab.com/joncol/hs-template/-/blob/main/README.org.

This article covers basic setup of development environment for Haskell in Emacs, using lsp-mode.

One way to do Haskell programming in Emacs these days is to use lsp-mode. When working on several projects, possibly using different versions of GHC, dependency management quickly becomes difficult to maintain. Nix can help with this.

This article supposes that you’ve already installed Nix on your system. We’ll quickly go through what’s needed to create a project skeleton using Cabal1, set up Nix for Haskell development, and then finish by giving an example of a Emacs configuration that will enable lsp-mode for editing Haskell source code.

A Cabal project

First create an empty directory for the project, and cd into it.

Then run cabal init:

nix-shell --pure -p ghc cabal-install \
    --run 'cabal init --cabal-version=3.0 -m -l BSD-3-Clause --libandexe'

Instead of --libandexe in the above, you can use only --lib, or --exe. You can also supply a custom --package-name, if you don’t want to use the name of the current directory.

Nix setup

Pin a version of nixpkgs using niv2:

nix-shell -p niv --run 'niv init -b nixpkgs-unstable'

Note that we’re using the unstable branch of nixpgs to get access to a newer version of GHC and other tools, than what’s available on stable.

Now, create a default.nix file3:

cat << 'EOF' > default.nix
{ sources ? import ./nix/sources.nix,
  pkgs ? (import sources.nixpkgs {})
}:

let
  t = pkgs.lib.trivial;
  hl = pkgs.haskell.lib;

  pkg = pkgs.haskellPackages.developPackage {
    root = ./.;

    modifier = (t.flip t.pipe)
      [hl.dontHaddock
       hl.enableStaticLibraries
       hl.justStaticExecutables
       hl.disableLibraryProfiling
       hl.disableExecutableProfiling];
  };

in { inherit pkg; }
EOF

In the above, the compiler version will be set to whatever is available in the nixpkgs repository. Note that haskellPackages is an alias for a specific version of GHC. For instance, at the time of writing, in nixpkgs-unstable, it is equivalent to haskell.packages.ghc8104. Selecting the GHC version by explicitly setting this to something else will cause a lot of compilation to occur, so it’s better to just use the alias haskellPackages4.

Next, create a shell.nix file:

cat << EOF > shell.nix
{ sources ? import ./nix/sources.nix,
  pkgs ? (import sources.nixpkgs {})
}:

let
  def = import ./default.nix {};
  dev-pkgs = with pkgs.haskellPackages;
    [ cabal-install
      ghcid
      haskell-language-server
      hlint
      ormolu
    ];

in def.pkg.overrideAttrs (attrs: {
  src = null;
  buildInputs = dev-pkgs ++ attrs.buildInputs;
})
EOF

For additional ergonomics, you can use direnv’s Nix integration. This lets you automatically get the right version of GHC when you cd into a project directory. To enable direnv’s Nix integration, create a .envrc file:

cat <<'EOF' > .envrc
[[ -n "${DIRENV_ALLOW_NIX}" ]] && use nix

if [ -e .envrc-local ]; then
   source .envrc-local
fi
EOF

The above requires you to set an environment variable DIRENV_ALLOW_NIX. This can be done by putting the following in your .zshenv file5:

export DIRENV_ALLOW_NIX=1

The reason for introducing the above environment variable is that the direnv integration will slow down navigation in the terminal, so it might be nice to have it be opt-in.

After setting the environment variable, enable the settings in .envrc by running:

direnv allow

With the Nix and direnv integration in place, you can verify that the version of GHC is what you expect:

ghc --version

This should result in the following, independently of any version of GHC you may have installed system-wide:

The Glorious Glasgow Haskell Compilation System, version 8.10.4

(Or whatever version is available in your version of nixpkgs.)

You can also do a build of your project, using nix-build:

IN_NIX_SHELL= nix-build -A pkg

The result of the above nix-build will be placed in a symlinked directory ./result/bin.

Using haskell-implicit-hie

To avoid warnings (in Emacs) about missing modules when later adding package dependencies, you also need a hie.yaml file. This is most easily created via the gen-hie command, which is part of haskell-implicit-hie (there should be a package available for your OS):

gen-hie > hie.yaml

Emacs setup

The following is an example of a basic Emacs setup of the relevant modes that are useful for Haskell development, using use-package6. The below also contains some evil-mode specific settings, but it should be easy enough to adapt to your own circumstances.

(use-package lsp-haskell
  :defer t
  :init
  (add-hook 'haskell-mode-hook
            (lambda ()
              (lsp)
              (setq evil-shift-width 2)))
  (add-hook 'haskell-literate-mode-hook #'lsp))

(use-package lsp-mode
  :hook (prog-mode . lsp-mode)

  :init
  (with-eval-after-load 'lsp-mode
    (evil-leader/set-key
      "l" lsp-command-map))

  :config
  ;; This is to make `lsp-mode' work with `direnv' and pick up the correct
  ;; version of GHC.
  (advice-add 'lsp :before #'direnv-update-environment)
  (setq lsp-modeline-code-actions-enable nil))

(use-package lsp-ui
  :hook (prog-mode . lsp-ui-mode)
  :config
  (evil-leader/set-key "x m" #'lsp-ui-imenu)
  (setq lsp-ui-doc-position 'bottom))

Now you should be able to open up Main.hs in your newly created project, and lsp-mode should automatically start. To inspect the status of LSP, it’s useful to switch to the buffer *lsp-haskell::stderr*. It should contain any relevant error messages, if something went wrong.

It’s also recommended to configure company-mode or similar to get autocompletion. We won’t cover this here.

Conclusion

You should now have the features of lsp-mode available within Emacs, with commands such as lsp-execute-code-action, code navigation, autocompletion, documentation, lenses (via lsp-lens-mode) etc.

You should also get automatic access to the correct version of GHC and other tools, with the help of Nix and direnv.

Here is a screenshot of how some of this might look:

I hope this was helpful, feel free to reach out with any feedback. Suggestions of improvements to the above are welcome.


  1. The need for using Stack pretty much goes away, when using Nix for the management of package versions. ↩︎

  2. GitHub - nmattia/niv: Easy dependency management for Nix projects ↩︎

  3. Note that these snippets use bash “heredocs”. You should be able to copy and paste the examples into your command-line prompt, and a file with the correct contents should be created. ↩︎

  4. Also see: Exploring Nix & Haskell Part 1: Project Setup – Christian Henry. ↩︎

  5. Assuming you’re using zsh of course. ↩︎

  6. GitHub - jwiegley/use-package: A use-package declaration for simplifying your… ↩︎

See also