NOTE (2022-06-16): I’ve switched to use Nix Flakes instead. See https://gitlab.com/joncol/hs-template/-/blob/main/README.md.
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 niv
2:
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 haskellPackages
4.
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-package
6. 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.
The need for using Stack pretty much goes away, when using Nix for the management of package versions. ↩︎
GitHub - nmattia/niv: Easy dependency management for Nix projects ↩︎
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. ↩︎Also see: Exploring Nix & Haskell Part 1: Project Setup – Christian Henry. ↩︎
Assuming you’re using
zsh
of course. ↩︎GitHub - jwiegley/use-package: A use-package declaration for simplifying your… ↩︎