Haskell, Eglot, Language Servers, and Emacs

1. Haskell Coding in Emacs

Haskell is a programming language where the rate of development and the evolution of its tooling can make it challenging to love. But love it I do, even though in many ways it is the antithesis of Common Lisp. If the latter is a forever language it sometimes feels as if Haskell is a never language, because I spend so much time trying to figure out how to get it working (admittedly I am only an intermittent user) that by the time I am done, I never write any code.

But here I am once again hoping to go to ZuriHac 24, and I would like to have a basic functioning environment by the time I get there.

The memory of the internet is long and that doesn't play well with the constantly changing tooling of emacs and Haskell. I had a lot of trouble finding contemporary guidance. These are my breadcrumbs as of April 2024 for a minimal functional set up using Emacs as the editor.

First you need Haskell. You will have to look to your distribution for how to get that installed. I am using Archlinux. At one point it was the go to distribution for Linux users, but with a change a few years ago static to dynamic linking things stopped working smoothly. Now the go to Linux for Haskell users is probably NixOS, but that will not be a simple transition, and I think the much simpler way to go is ghcup. Ghcup is a tool that allows you to install multiple versions of cabal (a packaging tool and more), stack (an alternative or even complementary packaging tool), and ghc the compiler. You can also switch between versions. In addition, you can use ghcup to get the haskell language server that matches each ghc version you install, and an haskell-language-server-wrapper should track the changes you make when switching between installed ghc versions.

Here is the warning. If you have not used ghcup before, and you decide to use it now clean out your old haskell installation and start with a clean slate. Then, install ghc, cabal, and a compatible haskell-language-server-wrapper using ghcup.

Eglot is now a part of Emacs (I think it came in 29, but I am using 30.50 here). Eglot is the Emacs polyglot. It provides a consistent interface to various language servers. You can use it for all your programming languages. Here I am only looking at Haskell. If you are using modern Emacs there is nothing to install. You just require it. In addition, I installed haskell-mode. I don't know if it is necessary, but it doesn't seem to be hurting anything.

Basic eglot configuration was very minimal, and a lot of this is not strictly necessary. Here is the snippet in my .init.el file:

(use-package eglot
  :config
  (add-hook 'haskell-mode-hook 'eglot-ensure)
  :custom
  (eglot-autoshutdown t)  ;; shutdown language server after closing last file
  (eglot-confirm-server-initiated-edits nil)  ;; allow edits without confirmation
  )

Some gotchas. PATH Variables for both your Linux system and your Emacs installation. In order to find the tools you installed with ghcup your system needs to know where to look for them. Mine are in ~/.ghcup/bin/. There are many ways to add things to the path of a Linux system, but I chose to use a method described in the environment variables Arch wiki page. It looks like this:

  set_path(){

    # Check if user id is 1000 or higher
    [ "$(id -u)" -ge 1000 ] || return

    for i in "$@";
    do
        # Check if the directory exists
        [ -d "$i" ] || continue

        # Check if it is not already in your $PATH.
        echo "$PATH" | grep -Eq "(^|:)$i(:|$)" && continue

        # Then append it to $PATH and export it
        export PATH="${PATH}:$i"
    done
}

set_path ~/.ghcup/bin

You also must make sure your Emacs path also gets the message. I mistakenly believed that Emacs would inherit my system path, especially since I could find the programs using which in an eshell session running inside Emacs. Not so. And Emacs has different paths. There are paths for loading libraries, executing things that Emacs manages for itself, internals if you will, and then the path for system executables. This is the one we want. There is a concise little library in MELPA that allows you to do this exec-path-from-shell. If you run emacs server, as I do, as a systemd --user unit you will need that daemonp line.

(use-package exec-path-from-shell)
(when (daemonp)
  (exec-path-from-shell-initialize))

The next gotcha: Compiling. You can easily do this from your terminal if you wish, but it would be nice to do it inside emacs. Given some key conflicts I needed to remap the "C-c C-c" key that my muscle memory insists must be the compile command to the "haskell" compile command.

(use-package haskell-mode
  :bind (:map haskell-mode-map
              (("C-c C-c" . haskell-compile))))

Penultimate gotcha: configuring the specific language server features and plugins you want to use with eglot.

I found this the hardest to get right, since I do not really know what all the haskell-language-server offers in the way of plugins, and what all the configuration variables are. The language servers use json, but emacs tends to use alists and plists, and in this situation there is a blend. You may find a lot of trial and error is required, and the advice I read suggests doing this local to a project and not globally. In each project directory write a file .dir-locals.el. Emacs info has a node on directory local definitions. This is an alist, but the eglot tool looks for a plist, so, as an example, you end up with something like this:

((nil . ((eglot-workspace-configuration . (:haskell
                                           (:plugins
                                            (:stan (:globalOn :json-false)
                                                   :ghcide-code-actions-fill-holes (:enabled t))))))))

Last gotcha: You need to sort of remember or figure out how cabal works and what it expects. I recommend for a practice project creating a directory with some name, e.g. tesths. Inside that directory, probably from a terminal, run cabal init. Then move to the ./app/ sub-directory and write your Test.hs file, maybe something like:

module Main where 


main :: IO ()
main = putStrLn "howdy"

Cabal expects a main for executable so just leave it like this to get started. Then you should be able to "C-c C-c" and haskell-language-server-wrapper will figure out to use cabal to build your executable. That will end up somewhere. Depending on exactly how you invoked the compilation it may be deep in .dist-newstyle sub-directory or more likely stored in a "store" in a sub-directory of a parent that looks like this: ~/.cache/hie-bios/dist-tesths-c1e9a7a89739f25f6dd9edc617b06f68.

If this doesn't work for you, hopefully it will help save you some time. And if you have a better way to do it please let me know. Thanks.

Date: 2024-04-19 Fri 00:00

Author: Britt Anderson

Created: 2024-04-23 Tue 05:19

Validate