Installation and Tools
The Haskell tooling story has improved enormously in the last five years, but it carries some historical baggage. If you read a tutorial from 2015, it probably tells you to install something called the "Haskell Platform," argues at length about whether to use cabal or stack, and sends you down a path that no longer matches how anyone sets up Haskell today. Ignore all of that. The current answer is: install GHCup, let it install everything, and use cabal unless you have a specific reason not to.
This page covers the actual setup, the tools you'll use day-to-day, and a quick tour of the GHCi REPL workflow that distinguishes Haskell development from most other languages.
GHCup Is the Installer
GHCup is the official cross-platform installer. It manages versions of GHC (the compiler), cabal (the build tool), stack (the alternative build tool), and HLS (the language server). One command installs all of them and sets up your shell.
curl --proto '=https' --tlsv1.2 -sSf https://get-ghcup.haskell.org | sh
This drops a ghcup binary in ~/.ghcup/bin and adds the right paths to your shell config. After that you can pick versions:
ghcup tui # interactive picker
ghcup install ghc 9.6.4
ghcup set ghc 9.6.4
ghcup install hls
The TUI is the easiest way to manage versions. Older tutorials might tell you to use your system package manager (apt, brew). Don't. GHC versions matter, and Haskell projects are typically pinned to a specific compiler version. GHCup handles this; package managers don't.
GHC vs GHCi
GHC is the compiler. It produces native binaries, generally good ones — Haskell binaries are competitive with Java or Go in throughput, slower than C++ or Rust in tight loops.
GHCi is the interactive REPL that comes with GHC. It loads modules, evaluates expressions, prints types, runs tests. You'll spend much more time in GHCi than running standalone executables, especially while developing.
$ ghci
GHCi, version 9.6.4: https://www.haskell.org/ghc/ :? for help
ghci> 1 + 1
2
ghci> :type map
map :: (a -> b) -> [a] -> [b]
ghci> :set +t -- show type of every result
ghci> "hello"
"hello"
it :: String
runghc (or runhaskell) compiles and runs a single file in one step, useful for scripts. ghc --make Foo.hs compiles to a binary. Most of the time, none of this matters because you'll be running everything through cabal.
Cabal vs Stack: A Brief History
For years, the Haskell community had two competing build tools, and choosing between them was the first decision a beginner had to make. Some context, because the choice still comes up.
Cabal (Common Architecture for Building Applications and Libraries) is the original. It's been around since the early 2000s and is the official tool from the Haskell Foundation. For a long time, cabal had a problem nicknamed "cabal hell" — installing one library could break another, because all packages went into a global package database and conflicted constantly.
Stack appeared around 2015, built by FP Complete, to solve cabal hell. It introduced the concept of "resolvers" — curated sets of package versions known to compile together, called Stackage snapshots. Stack also defaulted to per-project sandboxes. For several years, Stack was the right answer for new projects.
Cabal got better. Around 2017-2018, cabal added the v2- commands (now just the default) which use Nix-style isolated builds. Cabal hell, in practice, is no longer a problem. Modern cabal handles per-project dependency resolution cleanly.
Today, the practical answer is use cabal unless you have a reason not to. The Haskell Foundation has standardized on it, most libraries optimize for it, and the GHC team uses it. Stack is still maintained and works fine — IOG/Cardano uses Stack, for instance — but if you're starting fresh, cabal is the default path.
# create a new project
cabal init --interactive
# build
cabal build
# run the executable
cabal run
# enter ghci with the project loaded
cabal repl
# install a package globally as a binary
cabal install pandoc
A typical myproject.cabal file looks like this:
cabal-version: 3.0
name: myproject
version: 0.1.0.0
build-type: Simple
library
exposed-modules: MyLib
build-depends: base ^>=4.18.0.0,
text,
containers
hs-source-dirs: src
default-language: Haskell2010
executable myproject
main-is: Main.hs
build-depends: base ^>=4.18.0.0,
myproject
hs-source-dirs: app
default-language: Haskell2010
The ^>= operator means "compatible with this version" — it allows patch and minor bumps but locks the major version. This is the convention. Learn to read it.
Haskell Language Server (HLS)
HLS is the language server. If you're using VS Code, Emacs, Vim/Neovim, or any editor with LSP support, install HLS through GHCup and your editor will pick it up. You get type-on-hover, jump-to-definition, autocomplete, and inline error reporting — all the things a modern IDE provides.
ghcup install hls
ghcup set hls latest
For VS Code, install the "Haskell" extension and it will use HLS automatically. Make sure your project has a cabal.project or stack.yaml so HLS knows what compiler version to use.
HLS used to be slow on large projects. It's gotten faster but it can still be heavy on memory — expect to give it a couple of GB on a sizeable codebase. This is the price of the type-driven feedback loop. It's worth it.
Creating a Project
The shortest route to a working project:
mkdir hello
cd hello
cabal init --non-interactive --is-executable
cabal run
This creates a project skeleton with app/Main.hs, a .cabal file, and the right metadata. cabal run compiles and runs the executable. From here you add modules under src/, declare them in the library section of the cabal file, and import them.
For a library with tests:
library
exposed-modules: MyLib
hs-source-dirs: src
build-depends: base, text
test-suite myproject-test
type: exitcode-stdio-1.0
main-is: Spec.hs
hs-source-dirs: test
build-depends: base, myproject, hspec
Then cabal test runs the test suite. Most projects use hspec or tasty for test frameworks; both are fine.
The GHCi Workflow
Haskell development is heavily REPL-driven. You don't write a file, save it, run it, see an error, edit, repeat. You keep GHCi open, edit a file, type :reload, and try things at the prompt.
$ cabal repl
ghci> :load src/MyLib.hs
ghci> :type someFunction
someFunction :: Int -> String
ghci> someFunction 42
"forty-two"
ghci> :reload -- after editing the file
Useful GHCi commands worth committing to memory:
:type expr -- show the type of an expression
:info Name -- show info about a name (typeclass instances, etc.)
:reload -- reload modules after editing
:browse Module -- list everything exported by a module
:set +t -- print types of all results
:set -Wall -- enable all warnings
:doc Name -- show Haddock documentation
The combination of :type and :reload is the heartbeat of Haskell development. You write a function with a hole in it (literally _ or undefined), ask GHC what type it expects, and fill it in piece by piece. This is sometimes called "type-driven development" and it's a real shift from how most languages work.
-- start with this
parseUser :: String -> Maybe User
parseUser input = _
-- ghci tells you the hole has type Maybe User
-- so you start filling it in, asking the type system
-- what each piece needs to be
Common Pitfalls
Installing GHC through homebrew or apt. Don't. Use GHCup. The system package versions lag and don't coordinate with cabal/stack version requirements.
Mixing stack and cabal in the same project. Pick one. They have different conventions for project layout and they will fight if you try to use both.
Ignoring HLS errors. If the language server is showing red squigglies, fix them before you do anything else. Haskell's error messages are notoriously verbose but they almost always contain the answer somewhere in the wall of text.
Skipping -Wall. Turn on warnings everywhere. Haskell's warnings catch things like incomplete pattern matches and unused imports that you genuinely want to know about. Add ghc-options: -Wall -Wcompat to your cabal file.
Fighting cabal's version constraints. When cabal says "could not resolve dependencies," resist the urge to relax constraints randomly. Read the actual conflict, usually one package needs an old version of text or bytestring. The fix is usually to upgrade or wait for the maintainer to update.
Key Takeaways
GHCup is the installer. Cabal is the build tool. HLS is the language server. Install them with GHCup, point your editor at HLS, and use cabal repl for interactive development. The cabal-vs-stack debate is largely historical; modern cabal handles per-project isolation well and is the default path. The REPL is central to Haskell workflow — :type and :reload are commands you'll type a hundred times a day.