Hello, Haskell: Getting Started in 2026

11 min read
Featured image
Photo by Thomas T on Unsplash

Read this if you want to:

  • see beginner examples that link Haskell’s syntax to its underlying mental models – types, pattern matching, declarative style and do
  • see examples of Haskell’s distinctive syntax in practice
  • learn what Robin Milner’s work changed in functional programming
  • write and run code as you go

Introduction

In the current AI era, Rust and Python draw the most attention from learners and employers. Haskell, by contrast, remains niche: it trends lower on Google Trends and has far fewer job listings on LinkedIn. When roles do appear, the bar can be high enough that newcomers struggle to get started.

Meanwhile, LLMs now write a growing share of code, and developers are unusually uncertain about the profession’s direction. Interest in functional programming has also cooled since 2022 across employers and employees. The pragmatic objection recurs: strong FP developers are scarce, and their rates often do not look justified for the work being built.

Against that backdrop, hype is a poor guide. Learning tends to last longer when it is driven by intellectual interest rather than short-term demand. Haskell suits readers drawn to type-driven design and compact, algebraic notation.

A common observation is that a language may appear strange at first; once learned, other languages can begin to look unfamiliar. The same pattern appears when learning ideas such as type classes and tagless final in Scala: explanations often fail to “click” when the original context has been stripped away. Much of this material was imported from Haskell, adapted with trade-offs and additions, and then presented in isolation; returning to the source often clarifies what the abstraction is doing.

Setting the foundations

Tooling and workspace

Haskell is often approached a few times over the years. In 2026, the basic setup is clear enough to reduce churn, but tooling still decides whether time goes into learning the language or debugging editors.

JetBrains IDEs are often the default for consistent shortcuts and workflow. That works well for Scala in IntelliJ, Python in PyCharm, C++ in CLion, and React/JS in WebStorm. For Haskell, however, there is no comparable “it just works” experience: the plugins evaluated were either obsolete or unreliable, and time can be lost to tooling rather than to concepts.

By contrast, VS Code with a Haskell extension is a reliable default. Shortcuts are discoverable via the Command Palette; the UI shows each command’s shortcut. A dedicated learning repository helps isolate experiments. A common pattern is sandbox-<language-name>, for example sandbox-haskell, kept private to reduce the risk of accidental disclosure.

Organizing practice

The folder structure used here is:

tree -L 2
.
├── README.md
├── src
│   ├── Books (1)
│   ├── codeforces
│   ├── euler
│   ├── finalprojects
│   ├── goals
│   ├── h99
│   ├── spoj
│   └── Wiki (2)
├── stack.yaml
├── stack.yaml.lock
└── test
    └── Spec.hs

The structure separates src/Books and src/Wiki to support two loops: working through books and maintaining a personal reference.

Book exercises are typically organised as:

 tree -L 2
.
└── ProgrammingInHaskell
    ├── Ch01.hs
    ├── Ch02.hs
    ├── Ch04.hs
    └── Ch05.hs

A wiki reference is typically organised as:

 Algorithms
    └── Sort.hs

Haskell syntax is close to mathematical notation, so some algorithms end up looking notably close to pseudocode. The wiki can serve as a reference: “What does idiomatic Haskell look like?” Over time it reduces the cost of relearning details and limits exposure to low-quality patterns from LLMs.

Installing the toolchain

Installation is OS-specific. On Fedora, the setup was:

gmp-devel provides the headers for the GMP library, which GHC and many packages rely on for efficient big-integer arithmetic.

 sudo dnf install gmp-devel

GHCup is the de facto toolchain installer/manager for Haskell: it can install and switch between versions of GHC, Cabal, Stack, and HLS.

curl --proto '=https' --tlsv1.2 -sSf https://get-ghcup.haskell.org | sh

The compiler version used here is 9.8.4.

ghcup install ghc 9.8.4
ghcup set ghc 9.8.4

A first Haskell file typically uses the .hs extension. The examples below use ./src/Hello.hs, opened in VS Code to enable editor feedback and type hints.

AI as an auxiliary tool

Copilot is generally better to avoid at this point: it reduces active thinking and slows the formation of a mental model.

LLMs are most useful as a secondary tool for troubleshooting tooling, installation, and confusing error messages.

After solving an exercise, comparing it with an LLM solution can also be useful.


First steps in GHCi

With a Haskell extension in VS Code and GHC installed, a minimal module is enough to confirm that the toolchain works end to end. Early on, keeping examples in a single file and loading them via ghci lets you iterate quickly without a full build.

GHCi is Haskell’s interactive REPL: it loads modules, type-checks and evaluates expressions, and supports fast iteration. For example:

./src/Hello.hs:

add x y = x + y

A typical session starts from the src directory:

cd src
ghci

Once GHCi starts, the file is loaded:

:load Hello.hs

Shorthand: :l Hello.hs. After loading the file, the add function is available. After editing the file, it is reloaded with:

:reload

or the shorthand :r.

The function can then be called by name:

add 1 2

Output: 3

Verifying the setup

A short sanity check that the environment is wired correctly:

  • ghci runs in the src directory
  • :load Hello.hs loads the file
  • add 1 2 returns 3

Types as the surface of programs

In Haskell, everything is expressed in terms of values and functions. There’s no mutation in the “update a variable” sense, which is what people mean when they call Haskell a pure functional language. By contrast, in a multi-paradigm language like Scala, it’s possible to write var x = 5 and later update it to x = 6. That style is not how Haskell code is structured, and it changes how programs are reasoned about.

A second function helps clarify terminology. The distinction between “method” and “function” matters in Haskell’s model.

In Haskell, an expression such as add x y = x + y defines a pure function. It is a function rather than a method because it does not belong to a class or an object: there is no implicit receiver (this) and no dot-syntax. It is simply a mapping from inputs to an output.

The function is pure because it has no side effects and is deterministic: for the same inputs, it always produces the same result. This property allows the expression to be reasoned about mathematically and makes its behaviour predictable and testable.

“Methods” do exist in Haskell, but they’re tied to type classes, roughly corresponding to Scala typeclasses. That is a different mental model than object-oriented methods. These examples use plain functions:

double x = x * 2

This function doubles its input. No types are written here because languages in the ML family introduced type inference. Robin Milner and others introduced type inference in the 1970s, and the compiler can often infer types automatically. Counterintuitively, the result is concise code without giving up static guarantees.

To define types, Haskell uses the :: syntax:

double :: Int -> Int
double x = x * 2

Repeating the name – once for the type signature and once for the definition – can look “anti-DRY”. In practice, it separates the declaration from the implementation and makes the code easier to reason about. It also keeps the abstract level of types separate from implementation details.

From a more mathematical point of view, Haskell treats types like a form of logic: the type signature is a theorem, and the function body is a proof.

When should type signatures be written, and when should inference be used?

While learning, types are often omitted initially and generated by the editor. Inferred types can be more general, and more complex, than expected because polymorphism introduces typeclass constraints. A typeclass constraint is a requirement that a type supports a particular set of operations, such as ordering for sorting.

Expressiveness as structure

Decomposition with patterns and recursion

Pattern matching is a central readability feature. It is used directly in a function definition and is more capable than in many mainstream languages.

For example, here is a function that sums a list:

mySum [] = 0
mySum (n : ns) = n + mySum ns

This definition begins with the base case. When the list is empty, it returns 0.

The second clause handles the general case. Pattern matching splits the input into one element n and the rest of the list ns, which can still be empty.

Function calls don’t require parentheses; the typical form is nameOfFunction arg1 arg2 ....

In this example, mySum calls itself – classic recursion.

A common question is whether this is stack-safe and performant. In general, the naive version is not stack-safe. However, idiomatic Haskell uses a strict fold like foldl', or an explicit accumulator, to avoid building up thunks, or deferred computations. For summing there are built-in helpers already.

A rough progression from teaching examples to production code:

  • recursion replaces loops
  • but folds replace recursion
  • and strict folds replace naive folds

Declarative formulations

Declarative style supports reasoning: it describes what is needed, not how to do it step-by-step.

Quicksort illustrates the declarative style:

qsort :: (Ord a) => [a] -> [a] -- (1)
qsort [] = [] -- (2)
qsort (x : xs) = qsort smaller ++ [x] ++ qsort larger -- (3)
  where -- (4)
    smaller = [a | a <- xs, a <= x]
    larger = [a | a <- xs, a > x]
  1. Read the type declaration backwards
    • [a] - list of elements of type a - a is any type like Int, String, or custom types like Person, and the list contains those values, for example a list of ints
    • [a] -> [a] - the function expects a list of a and returns a list of a
    • (Ord a) => - we accept any type a, as long as it has an Ord instance so we can compare/order values of that type
  2. It defines sorting for an empty list (the trivial case)
  3. It uses pattern matching to split the list into one element x and the rest (xs)
    • This is the part where the algorithm looks like a standard quicksort definition.
    • Recursion sorts the smaller part, concatenates it with the one-element list [x], and then concatenates the sorted larger part
  4. where defines what is meant by smaller and larger
    • We use [a | a <- xs, a <= x], a list comprehension that looks like mathematical set-builder notation. That’s one reason mathematicians often enjoy Haskell.

Is this faster than C++/Java? Typically not.

It is mainly an example of expressive syntax. However, performance-critical operations typically rely on more specialized structures and primitives. Under the hood, [a] is a linked list, and append is (O(n)), which makes this particular quicksort formulation expensive.

Is it possible to implement a fast quicksort in Haskell? Yes – but it requires addressing the bottleneck and using something more appropriate, like Data.Vector / Data.Array, or using a well-tested library implementation.

This shows a familiar FP trade-off: purity supports reasoning and the modelling of business rules, yet mission-critical performance often requires controlled mutation.

This separation is generally less direct in Java/C++ when separating mutation from interface.

Sequencing effects with monads

The next topic is sequencing actions that may involve side effects, such as reading characters from input.

Instead of hard-coding a specific effect type, this is written generically. The Monad typeclass provides a way to sequence computations within a context m.

The type is:

seqn :: (Monad m) => [m a] -> m [a]

For example: seqn [getChar, getChar, getChar], where getChar is a standard function that reads a character from standard input in Haskell.

The do notation provides syntactic sugar for sequencing monadic operations, combined here with pattern matching and a base case:

seqn :: (Monad m) => [m a] -> m [a]
seqn [] = return [] -- (1)
seqn (act : acts) = do -- (2)
  x <- act
  xs <- seqn acts
  return (x : xs) -- (3)

There are a couple of new elements here:

  1. <- is called monadic bind operation. It runs the monadic action act, extracts its result, and binds it to x while staying in the context of the monad, for example the console input context.
  2. do provides a common way to write a sequence of monadic operations. First act is run, for example getChar, and its result is bound to x. Then the same function is called on the remaining actions. Finally, x and xs are combined, and return is used to produce m [a]; without return, the result would be [a], which would not match the type signature.

Summary

This is the first part of a longer series.

This section covered setting up a Haskell project in VS Code.

It implemented several examples with types and functions, and demonstrated how expressive Haskell is with pattern matching and declarative syntax.

It also provided an initial example of monads and do notation.

Overall, this illustrates how Haskell’s type-driven style supports reasoning about program structure and behavior even in early, small examples.

References

  • Programming in Haskell – Graham Hutton