Haskell World logo Haskell WorldWrite code, build worlds
Developer Tools

Five Haskell Tools Every Serious Developer Keeps Close

The Haskell toolchain is smaller and more focused than the toolchains for languages with larger commercial ecosystems. This is mostly a good thing - there are fewer choices to make and more consensus around which tools are standard.

Five Haskell tools every serious developer keeps in their workflow

The Haskell toolchain is smaller and more focused than the toolchains for languages with larger commercial ecosystems. This is mostly a good thing - there are fewer choices to make and more consensus around which tools are standard. The tools that serious Haskell developers use regularly are well-maintained, well-understood, and integrate well with each other. Five of them come up consistently as the ones that make the biggest difference in day-to-day development work.

GHCi: the interactive compiler

GHCi is the first tool every Haskell developer should become comfortable with. It is the language's interactive read-eval-print loop, and it is unusually capable compared to REPLs in other languages. You can load your project's modules, evaluate expressions, inspect types, test functions with specific inputs, and reload changed modules without losing your session's state. For exploring unfamiliar library APIs, it is faster than writing and compiling test programs.

The most-used GHCi commands are a small set. :type (or :t) shows the inferred type of any expression, including complex ones involving polymorphism and type classes. This is often more informative than the documentation. :info shows detailed information about a name: for a function, it shows the type signature, the module it comes from, and any relevant typeclass instances. For a type, it shows the type definition and all instances defined for it. :browse ModuleName lists everything exported from a module.

In a project loaded via cabal repl, editing a source file and typing :reload recompiles the changed modules and updates the GHCi session. Combined with pattern-matching on test inputs to verify function behavior interactively, this gives a fast feedback loop for code exploration. GHCi's ability to evaluate pure functions with specific inputs is the fastest way to answer "what does this function do with edge case X?" without writing a test.

Haskell Language Server (HLS)

HLS is the IDE backbone for Haskell development. It provides the type-on-hover feature that is so central to working with Haskell effectively - seeing the full type of any expression under the cursor tells you more about what a piece of code does than reading the code itself in many cases. It provides go-to-definition for navigation, find-references for impact analysis, and inline error messages that appear as you type.

Beyond the standard LSP features, HLS provides Haskell-specific actions. The import helper suggests imports for unresolved names and can add them automatically. The refactoring actions include extract to a local variable, eta-reduce a function (remove trailing arguments that both sides use identically), and introduce a let binding. The hole-filling feature suggests completions for typed holes - expressions of the form _ that you place where a value should go - based on the expected type.

HLS performance varies by project size. For small to medium projects (up to a few hundred modules), it is responsive. For large projects, it can be slower to index on startup and occasionally slow on type queries in modules with complex type-level code. Keeping your project structured with a reasonable number of modules of reasonable size avoids the worst cases. HLS is actively developed and performance has improved substantially with each major release over the past few years.

Fourmolu: code formatting

Fourmolu is the formatter that has settled into the most common use across the Haskell community. It is based on Ormolu but adds a small set of configuration options that allow teams to set preferences like import grouping style, trailing commas in records, and indentation amount. The configuration is intentionally limited to prevent it from becoming a source of endless team debates about style.

The argument for a formatter is the same as in any language: remove style discussions from code review so that reviews can focus on substance. In Haskell specifically, there are enough ways to format the same code - particularly around multi-line expressions, record syntax, and import lists - that consistent formatting reduces cognitive load when reading unfamiliar code. Settling on Fourmolu once and enforcing it in CI removes the problem entirely.

Fourmolu integrates with HLS through the ormolu plugin, so format-on-save works in any editor that uses HLS for Haskell support. A fourmolu.yaml configuration file in the project root is picked up automatically. The CI check is a single command: fourmolu, check on the source files, which exits non-zero if any file is not correctly formatted.

HLint: code improvement suggestions

HLint analyzes Haskell code and suggests simplifications and improvements. It knows a large set of patterns: places where a manual fold can be replaced with a library function, where explicit recursion has a cleaner combinator expression, where imports are redundant, where the code does not match idiomatic Haskell style. Many of these suggestions are genuine improvements - the code becomes shorter, clearer, and more recognizable to other Haskell developers.

Running HLint on an existing codebase for the first time produces a long list of suggestions. Working through them is a learning exercise as well as a code quality improvement - each suggestion points to a pattern that the Haskell standard library handles well, and understanding why the suggested form is preferred teaches idiomatic Haskell incrementally. After the initial cleanup, running HLint in CI catches new issues before they accumulate.

HLint is configurable through a .hlint.yaml file. Suggestions that do not fit the project's style can be disabled. Some suggestions are genuinely debatable - whether to use point-free style, for example, is a style question without a clear right answer - and HLint can be configured to not suggest changes in those areas. The default configuration is sensible for most projects, but the ability to customize means teams can tune it to their standards.

Cabal with freeze files: reproducible builds

Cabal is both a build tool and a package manager, and the combination of its solver and freeze files provides the reproducible build capability that serious projects need. The cabal.project.freeze file records the exact versions of all dependencies chosen by the solver at a specific point in time. Committing this file ensures that everyone on the team and all CI runs use the same dependency versions.

The workflow for managing dependencies is: add a dependency to the cabal file, run cabal build without a freeze file to let the solver find compatible versions, verify the build and tests work, then run cabal freeze to capture the chosen versions. Commit the updated cabal file and the freeze file together. When upgrading dependencies, run cabal outdated to see what is available, update the cabal file's version constraints, delete the freeze file, let the solver run again, and re-freeze.

Cabal's build and test features cover the full development workflow. cabal build for compilation, cabal test for running the test suite, cabal haddock for generating documentation from Haddock annotations, cabal bench for running benchmarks. For projects that need to generate documentation for publication, cabal haddock, hyperlink-source produces hyperlinked documentation where every name links to its source definition. Having a single tool that handles all of these reduces context-switching and tool configuration overhead.

How these tools work together

The interaction between these tools defines the practical development workflow. A typical session starts with opening the project in an editor that has HLS active, which provides type information and error feedback as you write. You open a GHCi session in a terminal for rapid function testing. When you finish a change, HLint checks for improvements and Fourmolu formats the code. Cabal builds and tests verify correctness. The freeze file ensures that your environment matches CI.

This is a small, focused toolchain. Each tool does one job well. There is no configuration sprawl, no tool conflicts, no version compatibility matrices to manage beyond GHC and HLS. The simplicity is part of the value - the cognitive overhead of the toolchain is low enough that it stays out of the way of the actual development work. For developers coming from ecosystems with more complex toolchain situations, this clarity is one of the underappreciated advantages of working in Haskell.

AS
Adil Sato

Adil Sato teaches programming concepts at a community college and writes online guides for the questions students ask most in the second week of class, not the first. His focus is on making type systems, monads, and functional patterns understandable without reducing them to metaphors that break down the moment you try to use them for real work.

More posts by Adil

More from the blog