Haskell Development with Neovim

July 16, 2017

Configuring an editor for a new language is a double-edged sword: it’s intensely satisfying when done, but takes time away from diving into the language itself! After using Haskell for a little over a year, I’ve settled on a high-quality set of editor plugins. They’re simple, powerful, and all play nicely together.


I use Haskell Stack exclusively. Stack’s goal is reproducible builds, which means that in general, things Just Work.

I also use Neovim, rather than normal Vim. Usually, my justification is ideological rather than technical. However, for Haskell my setup requires Neovim. Fear not! Neovim is feature-packed and also very stable. I love Neovim, and I’ll be writing more about why in a future post.

By the way, new to Vim plugins? I happen to have just the post for you!


We’re going to move in order of increasing complexity. That said, even the most “complex” plugin here is actually quite painless to set up. By the end, we’ll have a complete development experience! Coming up:

  • syntax highlighting & indentation (haskell-vim)
  • auto-formatting & style (hindent, stylish-haskell)
  • quickfix and sign column support (using ale) for:
    • linter style suggestions (hlint)
    • compiler errors and warnings (ghc-mod)
  • Type inspection, REPL integration, and more! (intero-neovim)

To keep things concise, I’ve moved all the relevant configuration to the end of the post. For now, let’s start at the top.

Syntax Highlighting & Indentation

Vim’s default Haskell filetype plugin is pretty lack luster. Everything is blue, except for strings which are colored like comments, and keywords which are colored like constants. Indentation is wonky in some edge cases, and isn’t configurable.

This plugin corrects all that. It’s the filetype plugin for Haskell that should ship with Vim.

Not only does it come with saner defaults, it also comes with more config options, especially for indentation. This is important because it lets me tweak the automatic indentation to my own personal style.

(Remember: all the config is at the end of the post.)

Auto-formatting and Indentation

  • Plugin: vim-hindent
  • Tool: stack install hindent
  • Tool: stack install stylish-haskell

For small projects, I have an idea of what style I like best. However, for larger projects it’s unfair to ask contributors that they learn the ins and outs of my style. Situations like these call for automated solutions.

go fmt famously solved this problem for Golang by building the formatting tool into the compiler. For Haskell, there’s hindent2. hindent can be installed through Stack, and vim-hindent is a Vim plugin that shims it.

But I said I’m partial to my own style in personal projects. There’s another Haskell formatter that’s much less invasive: stylish-haskell. It basically only reformats imports, case branches, and record fields, aligning them vertically. And in fact, it’s possible to use this alongside hindent.

With these three tools, I can pick the right tool for the job:

  • Hand saw: let haskell-vim config control the indentation
  • Table saw: run stylish-haskell only
  • Chainsaw: run hindent only
  • Chainsaw, then sand paper: run hindent, then stylish-haskell

Getting them to play together requires a bit of config, so I’ve included mine at the end of the post.

Quickfix & Sign Columns

  • Plugin: ale
  • Tool: stack install hlint
  • Tool: stack build ghc-mod
    • N.B.: This is build not install here3.

This step requires either Neovim or Vim 8; ALE stands for “Asynchronous Lint Engine,” so it’s using the new asynchronous job control features of these two editors. It’s like an asynchronous Syntastic4.

ALE ships with a number of Haskell integrations by default. For example, it can show errors if only Stack is installed. I prefer enabling two of ALE’s Haskell integrations: hlint and ghc-mod.

  • hlint is a linter for Haskell. It warns me when I try to do silly things like if x then True else False.
  • ghc-mod is a tool that can check files for compiler errors.

The beauty of ALE is that it works almost entirely out of the box. The only real setup is to tell ALE to use only these two integrations explicitly. I’ve included the one-liner to do this in the config at the bottom.

Intero: The Pièce de Résistance

Intero is a complete development program for Haskell. It started as an Emacs package, but has been ported almost entirely to Neovim. Probably the best way to introduce it is with this asciicast:

Intero for Neovim asciicast

Intero is designed for stack, sets itself up automatically, has point-and-click type information, and lets me jump to identifier definitions. On top of it all, it uses Neovim to communicate back and forth with a terminal buffer so that I get a GHCi buffer right inside Neovim. For Emacs users, this is nothing new I’m sure. But having the REPL in my editor continues to blow my mind 😮.

Developing with the REPL in mind helps me write better code. Only top-level bindings are exposed in the REPL, so I write more small, testable functions. See here for more reasons why the REPL is awesome.

On top of providing access to the REPL, Intero provides about a dozen convenience commands that shell out to the REPL backend asynchronously. Being able to reload my code in the REPL—from Vim, with a single keystroke!—is a huge boon when developing.

Intero takes a little getting used to, so be sure to read the docs for some sample workflows. Intero also sets up no mappings by default, so I’ve included my settings below.

The Eagerly-Awaited Config

And without further ado…

" ----- neovimhaskell/haskell-vim -----

" Align 'then' two spaces after 'if'
let g:haskell_indent_if = 2
" Indent 'where' block two spaces under previous body
let g:haskell_indent_before_where = 2
" Allow a second case indent style (see haskell-vim README)
let g:haskell_indent_case_alternative = 1
" Only next under 'let' if there's an equals sign
let g:haskell_indent_let_no_in = 0

" ----- hindent & stylish-haskell -----

" Indenting on save is too aggressive for me
let g:hindent_on_save = 0

" Helper function, called below with mappings
function! HaskellFormat(which) abort
  if a:which ==# 'hindent' || a:which ==# 'both'
  if a:which ==# 'stylish' || a:which ==# 'both'
    silent! exe 'undojoin'
    silent! exe 'keepjumps %!stylish-haskell'

" Key bindings
augroup haskellStylish
  " Just hindent
  au FileType haskell nnoremap <leader>hi :Hindent<CR>
  " Just stylish-haskell
  au FileType haskell nnoremap <leader>hs :call HaskellFormat('stylish')<CR>
  " First hindent, then stylish-haskell
  au FileType haskell nnoremap <leader>hf :call HaskellFormat('both')<CR>
augroup END

" ----- w0rp/ale -----

let g:ale_linters.haskell = ['stack-ghc-mod', 'hlint']

" ----- parsonsmatt/intero-neovim -----

" Prefer starting Intero manually (faster startup times)
let g:intero_start_immediately = 0
" Use ALE (works even when not using Intero)
let g:intero_use_neomake = 0

augroup interoMaps

  au FileType haskell nnoremap <silent> <leader>io :InteroOpen<CR>
  au FileType haskell nnoremap <silent> <leader>iov :InteroOpen<CR><C-W>H
  au FileType haskell nnoremap <silent> <leader>ih :InteroHide<CR>
  au FileType haskell nnoremap <silent> <leader>is :InteroStart<CR>
  au FileType haskell nnoremap <silent> <leader>ik :InteroKill<CR>

  au FileType haskell nnoremap <silent> <leader>wr :w \| :InteroReload<CR>
  au FileType haskell nnoremap <silent> <leader>il :InteroLoadCurrentModule<CR>
  au FileType haskell nnoremap <silent> <leader>if :InteroLoadCurrentFile<CR>

  au FileType haskell map <leader>t <Plug>InteroGenericType
  au FileType haskell map <leader>T <Plug>InteroType
  au FileType haskell nnoremap <silent> <leader>it :InteroTypeInsert<CR>

  au FileType haskell nnoremap <silent> <leader>jd :InteroGoToDef<CR>
  au FileType haskell nnoremap <silent> <leader>iu :InteroUses<CR>
  au FileType haskell nnoremap <leader>ist :InteroSetTargets<SPACE>
augroup END

Wrap Up

With these tools, I feel empowered (rather than hindered) when I sit down to work with Haskell.

  • The entire setup uses Stack, so things Just Work.
    • As a consequence, everything works with the implicit global Stack project!
  • It scales up in power:
    • From simple syntax highlighting and manual indentation…
    • to an indentation chainsaw and a REPL embeded in the editor!
  • I can take full advantage of all my tools working together, leading to cleaner code and fewer frustrations.

Now that I’m finally at a point where I can stop fretting about my Haskell setup, I’ll have more time to explore the language and write about my experience.

Haskell-the-language isn’t quite on the same level as SML-the-language, but it’s far and above when comparing by tooling support. I’m looking forward to taking advantage of that!

Jake on the Web

If you cared enough to read that far, you should consider following me on GitHub or paying a visit to my homepage. If this post was about one of my open source projects, make sure to star it on GitHub! I love hearing what people think, so feel free to open an issue or send me an email.

  1. While listed under “neovimhaskell” on GitHub, this plugin works with normal Vim, too.

  2. Chris Done explains the appeal of solving style issues with tooling for Haskell well. The moral of the story is that hindent version 5 ships with only the most popular style formatter in an effort to arrive at a singular Haskell style: http://chrisdone.com/posts/hindent-5

  3. We want to install ghc-mod once in every project. It can be done globally, but it might get out of sync with the current project.

  4. Some people are familiar with Neomake for this task. However, Neomake is much more minimal than ALE. Neomake basically only builds, whereas ALE is more configurable and hackable.

Reach for Markdown, not LaTeX

Writing should be a pleasant experience. With the right tools, it can be. LaTeX is powerful but cumbersome to use. With Markdown, we can focus on our writing, and worry about the presentation later. Pandoc can take care of the presentation for us, so the only thing left to do is start. Continue reading

Troubleshooting Haskell Stack Setup on OS X

Published on August 03, 2016

Let’s Have a Chat about Encryption

Published on April 17, 2016