Haskell World logo Haskell WorldWrite code, build worlds
Developer Tools

Setting Up a Haskell Development Environment in 2026

Setting up a Haskell development environment has improved substantially over the past few years. What used to require navigating multiple incompatible toolchain managers with unclear relationships between them is now mostly handled by a...

Setting up a Haskell development environment in 2026

Setting up a Haskell development environment has improved substantially over the past few years. What used to require navigating multiple incompatible toolchain managers with unclear relationships between them is now mostly handled by a single installer. The rough edges are mostly gone for the initial setup, though there are still choices to make and a few things to know about that are not obvious from the documentation. This is what a working setup looks like in 2026.

GHCup: the right starting point

GHCup is the official toolchain installer for Haskell and the starting point for any new setup. It manages GHC (the compiler), Cabal (the build tool and package manager), Stack (an alternative build tool), and HLS (the Haskell Language Server). Running the GHCup installer from the official site handles the bootstrapping. On macOS and Linux, the installer is a shell script that you pipe to sh. On Windows, it is an executable installer.

After installation, run ghcup tui to open the interactive interface. This shows all available versions of each tool and the currently installed ones. For a fresh setup, install the recommended GHC version (labeled "recommended" in the TUI), the latest stable Cabal, and the corresponding HLS. The TUI lets you switch between installed versions, which is useful when contributing to projects that require a specific GHC version.

The recommended GHC version at the time of writing is GHC 9.8 or 9.10, depending on which HLS version you are using. HLS versions lag GHC releases slightly - a brand-new GHC version may not have HLS support immediately. Check the HLS release notes to confirm which GHC versions are supported before installing a GHC version whose main appeal is being the very latest.

Cabal versus Stack

New Haskell developers frequently encounter both Cabal and Stack and wonder which to use. The short answer: use Cabal. It is the default build tool maintained by the Haskell Foundation and the one you need to understand for interacting with most open source Haskell projects. Stack was created to address specific pain points that existed in older Cabal versions, but those problems have largely been fixed, and Cabal is now the better choice for most uses.

Stack has one genuine advantage that some teams prefer: reproducible builds through Stackage snapshot sets. A Stackage snapshot is a curated set of library versions that are known to compile together, eliminating version conflict resolution. If you are working in an organization that values hermetic builds and does not want to spend time resolving dependency conflicts, Stack with Stackage is a defensible choice.

Cabal's approach is more flexible: Cabal.project files can pin dependencies to exact versions, and the new solver in recent Cabal versions handles version resolution reliably. For most projects, Cabal with pinned dependencies provides the reproducibility that Stack's snapshot approach provides, with more flexibility when you need to use a package that is not in the snapshot.

Editor setup with HLS

The Haskell Language Server provides IDE features: hover types, go to definition, auto-complete, inline error messages, refactoring actions, and code formatting. It communicates with editors through the Language Server Protocol, which means it works with any editor that supports LSP. VS Code, Neovim, Emacs, and Helix all have working HLS integrations.

For VS Code, the Haskell extension in the marketplace handles HLS setup automatically. It detects the GHC version your project requires and downloads the appropriate HLS binary if it is not already installed. This automatic management means you rarely need to think about HLS versions explicitly. The extension also handles syntax highlighting, bracket matching, and the display of LSP diagnostics in the editor's error panel.

For Neovim, the recommended setup uses nvim-lspconfig with the Haskell configuration. Combined with a completion plugin like nvim-cmp and a diagnostics display like trouble.nvim, this produces an experience comparable to VS Code. The setup requires a bit more configuration but is well-documented in both the nvim-lspconfig repository and the Haskell Language Server documentation.

A common issue with HLS is slow startup time, particularly in larger projects. HLS builds an index of your project's types and definitions, which takes time proportional to project size. For large projects, the first indexing run after opening the project can take several minutes. Subsequent starts are faster because the index is cached. Keeping your project well-organized with a reasonable number of modules helps - projects with hundreds of very small modules index more slowly than projects with fewer, larger modules.

Project structure with Cabal

A Cabal project has two key files: the .cabal file that describes the package, and optionally a cabal.project file that configures the build. The cabal file lists the library and executables in your project, their dependencies, and their source directories. The cabal.project file is for multi-package projects and for project-wide settings like package version pins.

Initialize a new project with cabal init, interactive, which asks a series of questions and creates the initial cabal file. Add dependencies to the build-depends section of the cabal file. Run cabal build to compile, cabal run to run the main executable, cabal test to run tests, and cabal repl to open an interactive session with your project's code loaded.

The cabal.project.freeze file, generated by cabal freeze, records the exact versions of all dependencies that were selected by the solver. Committing this file to version control ensures that anyone building the project gets the same dependency versions. For CI/CD pipelines, this is important for reproducible builds. Updating dependencies means removing or updating the freeze file and running the solver again.

GHCi as a development tool

GHCi, Haskell's interactive REPL, is one of the most useful tools in the Haskell development workflow. Accessed with cabal repl, it loads your project and lets you evaluate expressions, inspect types, and test functions interactively. The :reload command reloads changed modules without restarting the session, making it practical for an edit-test loop where you modify a function and immediately test it in GHCi.

The :type command shows the type of any expression: :type map shows (a -> b) -> [a] -> [b]. The :info command shows detailed information about a type or function, including its definition and the typeclasses it belongs to. The :browse command lists all the exports of a module, which is useful for discovering what is available in a library you are using for the first time.

Multi-line input in GHCi uses the :{ and :} delimiters to open and close a multi-line input block. Alternatively, the :set +m setting enables multi-line mode where a blank line ends the current input. For experimenting with multi-line function definitions or do blocks, either approach works.

Formatting and linting

Fourmolu and Ormolu are the two standard Haskell formatters. Ormolu has no configuration options - it always produces the same output, which eliminates formatting debates but may not match team preferences. Fourmolu is a Ormolu fork that adds a small set of configuration options, enough to accommodate common style preferences without opening up endless configuration bikeshedding. Either is a reasonable choice; pick one, configure it in your editor's format-on-save setting, and enforce it in CI.

HLint is the standard linting tool. It identifies code that can be simplified - replacing explicit recursion with library functions, pointing out redundant parentheses, suggesting more idiomatic ways to express common patterns. Running HLint on a codebase for the first time often produces dozens of suggestions, many of which are genuine improvements. Addressing them is a good learning exercise as well as a code quality improvement. HLint can be run as a CI step to prevent new issues from accumulating.

Continuous integration

Setting up CI for a Haskell project is straightforward with GitHub Actions. The haskell-actions/setup action installs GHC and Cabal, and the standard build steps are cabal update, cabal build, all-targets, and cabal test, all-targets. Testing against multiple GHC versions by running the job in a matrix is standard practice for library projects, ensuring that the library compiles and tests pass on all supported GHC versions.

Build caching is important for keeping CI runtimes manageable. The standard approach caches the ~/.cabal directory, which contains downloaded package sources and compiled dependencies. With a warm cache, a CI run that builds only changed modules completes in a few minutes. Without caching, downloading and compiling all dependencies from scratch can take fifteen to thirty minutes depending on the project's dependency count.

FB
Finnian Burke

Finnian Burke spent eight years building distributed systems before leaving to write about the programming patterns that actually hold up under pressure. He has worked with Haskell, Rust, and Go in production, and writes mostly about the parts of functional programming that turn out to be useful regardless of the language you use day to day.

More posts by Finnian

More from the blog