Learn Haskell
Haskell is a pure, lazy, statically typed functional language born in 1990 and named after the logician Haskell Curry. It pairs Hindley-Milner type inference with a rich type-class system, algebraic data types, and pattern matching, so programs read like precise mathematical definitions yet compile to fast native code through GHC. Purity means functions have no hidden side effects, and effects such as input and output are modeled explicitly with types like IO, Maybe, and Either. Laziness lets you describe infinite structures and only pay for what you consume. The result is concise, composable code that often expresses an idea in fewer lines than imperative languages, while the compiler catches large classes of mistakes before the program ever runs.
Setup: GHC, GHCup, Cabal, and Stack
Install the Haskell toolchain and run your first program.
Haskell is compiled and run primarily through GHC, the Glasgow Haskell Compiler. You almost never install GHC by hand, instead you use GHCup, the official installer that manages GHC, the build tools, and the language server together.
On Linux or macOS the one-line installer sets everything up:
curl --proto '=https' --tlsv1.2 -sSf https://get-ghcup.haskell.org | sh
GHCup installs three things worth knowing:
- GHC - the compiler.
ghcbuilds binaries,runghc(also calledrunhaskell) interprets a script directly, andghciis the interactive REPL. - Cabal - the standard build tool and the interface to Hackage, the central package archive.
- Stack - an alternative build tool built on curated package sets called resolvers, popular for reproducible builds.
Start in the REPL to get a feel for the language:
$ ghci
GHCi, version 9.x
ghci> 2 + 3 * 4
14
ghci> map (* 2) [1, 2, 3]
[2,4,6]
ghci> :type "hello"
"hello" :: String
ghci> :quit
The :type (or :t) command asks GHCi to report a value's type, and :info describes a name. These are your most-used tools while learning.
A Haskell source file ends in .hs. The smallest complete program defines main:
main :: IO ()
main = putStrLn "Hello, Haskell!"
Save that as Hello.hs and run it directly with the interpreter:
runghc Hello.hs
For a real project, let Cabal scaffold it for you:
mkdir myapp && cd myapp
cabal init --non-interactive
cabal run
cabal init creates a .cabal file describing your package: its name, version, dependencies, and which modules form the executable. Adding a dependency is a one-line edit to the build-depends field, after which cabal build fetches it from Hackage and compiles everything.
If you prefer Stack, the equivalent flow is stack new myapp, then stack build and stack run. Stack pins an exact set of compatible package versions via a resolver such as lts-22.x, which makes builds reproducible across machines without extra effort. Cabal and Stack solve the same problem in different styles, and you can pick either one when starting out; the rest of this track uses plain GHCi and Cabal because they ship as the defaults.
Which tool should a beginner choose? If you just want to experiment, stay in ghci and runghc, no project files required. When you start writing something you will keep, reach for cabal init. The community is comfortable with both Cabal and Stack, and switching later is straightforward because both read a .cabal file.
GHCi has a handful of commands that make it a genuine workbench rather than a calculator. Beyond :type, you will lean on :reload (or :r) to recompile a file you are editing, :load File.hs (or :l) to bring a module into scope, :info (:i) to inspect a name, and :browse to list everything a module exports. A typical loop is: edit the .hs file in your editor, then press up-arrow and Enter on :r in GHCi to see the result instantly.
ghci> :load Hello.hs
[1 of 1] Compiling Main
Ok, one module loaded.
ghci> main
Hello, Haskell!
ghci> :reload
Ok, one module loaded.
For editor support, install the Haskell Language Server (HLS) through GHCup. With the official Haskell extension in VS Code you get type-on-hover, inline errors, and automatic refactors, which dramatically shortens the feedback loop while you are still learning the type system. HLS reads your .cabal or Stack configuration to understand the project, so it works out of the box once the project builds.
A quick note on GHC versions: GHCup can install several side by side and switch the active one with ghcup set ghc <version>, which is handy when a library only supports certain releases. For learning, the latest stable GHC plus the recommended Cabal is the right default, and GHCup marks those for you.
The official starting point is the Haskell homepage, which links the installer and the GHCi guide: https://www.haskell.org/get-started/
Types and Type Inference
Read type signatures and let Hindley-Milner do the work.
Haskell is statically typed: every expression has a type that the compiler knows before the program runs. What makes this pleasant rather than tedious is Hindley-Milner type inference. You can usually omit type annotations entirely and the compiler will deduce the most general type for you.
The double colon :: reads as "has type". You will see it in GHCi and in source files:
true :: Bool
true = True
count :: Int
count = 42
greeting :: String
greeting = "hi"
String is just an alias for [Char], a list of characters. A few core types you will meet immediately:
Bool-TrueorFalseInt- fixed-width machine integer;Integer- arbitrary precisionDouble- double precision floating pointChar- a single Unicode character[a]- a list of values all of typea(a, b)- a tuple pairing anawith ab
The lowercase a and b are type variables. A function that works for any type is polymorphic. The classic example is length:
length :: [a] -> Int
This says: given a list of any element type, return an Int. The compiler does not need to know what a is, because length never inspects the elements.
Function types use arrows. Read Int -> Int -> Int as "takes an Int and another Int, returns an Int":
add :: Int -> Int -> Int
add x y = x + y
Even if you delete the signature, inference recovers a type. Try this in GHCi:
ghci> let twice f x = f (f x)
ghci> :type twice
twice :: (a -> a) -> a -> a
The compiler figured out that twice takes a function from a to a and a starting value, with no hints from you. Writing the signature anyway is considered good style for top-level definitions, because it documents intent and turns vague type errors into precise ones pinned to the right line.
Numeric literals are themselves polymorphic, which sometimes surprises newcomers. The literal 5 has type Num a => a, meaning "any numeric type". The part before => is a type-class constraint, covered in a later lesson. It is why 5 can be used as an Int, an Integer, or a Double depending on context:
ghci> :type 5
5 :: Num a => a
ghci> (5 :: Double) / 2
2.5
Type annotations and the :: operator. When inference cannot decide a type on its own, you can pin one down inline. This is common with numeric literals and with read, which parses a string into whatever type the context demands:
ghci> read "42" :: Int
42
ghci> read "42" :: Double
42.0
The same :: Type suffix works on any expression and is how you resolve the occasional ambiguity the compiler reports.
Type aliases and new types. You can give a descriptive name to an existing type with type, which improves readability without changing behavior:
type Name = String
type Age = Int
introduce :: Name -> Age -> String
introduce n a = n ++ " is " ++ show a
A type alias is interchangeable with the type it names. When you instead want a distinct type that the compiler will not confuse with its underlying representation, you reach for newtype, introduced more fully alongside data declarations in a later lesson.
Reading compiler errors. Type errors are your main feedback while learning, so it helps to read them calmly. A message like "Couldn't match expected type 'Int' with actual type '[Char]'" means you used a string where a number was wanted. GHC points at the offending expression and names both the type it expected and the type it found. Because inference is principled, the reported types are precise, and adding a top-level signature often turns a confusing error into an obvious one by telling the compiler exactly what you intended.
Because types are checked thoroughly at compile time, a program that type-checks has already ruled out enormous categories of bugs: no null surprises, no passing a string where a number was expected, no forgotten return value. The slogan in the community is that in Haskell, if it compiles, it often just works.
The definitive reference for the type system and inference rules is the GHC users guide: https://downloads.haskell.org/ghc/latest/docs/users_guide/
Functions and Currying
Define functions, partially apply them, and compose pipelines.
Functions are the heart of Haskell. You define one by writing its name, its parameters, an equals sign, and the body. There is no return keyword, because a function simply is the expression on the right of the =.
square :: Int -> Int
square x = x * x
average :: Double -> Double -> Double
average a b = (a + b) / 2
Application is just whitespace, with no parentheses around the arguments: you write square 5, not square(5). Application binds tighter than any operator, so square 5 + 1 means (square 5) + 1.
Currying. Every Haskell function technically takes exactly one argument and returns a function that takes the next. The type Int -> Int -> Int is really Int -> (Int -> Int). This is why partial application works so naturally:
add :: Int -> Int -> Int
add x y = x + y
addTen :: Int -> Int
addTen = add 10 -- supply one argument, get a new function back
addTen is add with its first argument fixed at 10. Calling addTen 5 yields 15. Partial application turns general functions into specialized ones for free.
Sections let you partially apply operators by wrapping them in parentheses:
(* 2) -- a function that doubles
(10 -) -- subtract its argument from 10
(/ 2) -- halve its argument
These pair beautifully with higher-order functions like map, which applies a function to every element of a list:
ghci> map (* 2) [1, 2, 3]
[2,4,6]
ghci> filter (> 3) [1, 5, 2, 8]
[5,8]
Anonymous functions (lambdas) use a backslash, chosen because it resembles the Greek lambda. \x -> x + 1 is a one-off increment function:
ghci> map (\x -> x * x + 1) [1, 2, 3]
[2,5,10]
Composition glues functions together with the . operator, reading right to left. (f . g) x equals f (g x):
describe :: Int -> String
describe = show . square -- square first, then convert to a String
The related $ operator is function application with very low precedence, used to drop parentheses. f $ g x means f (g x):
ghci> putStrLn $ "result: " ++ show (square 9)
result: 81
Local definitions come from let ... in expressions or trailing where clauses. where is idiomatic for helper bindings attached to a function:
roots :: Double -> Double -> Double -> (Double, Double)
roots a b c = (x1, x2)
where
d = sqrt (b * b - 4 * a * c)
x1 = (-b + d) / (2 * a)
x2 = (-b - d) / (2 * a)
Notice how d, x1, and x2 read like the lines of a math derivation. Because everything is an expression and functions are first class, you tend to build small reusable pieces and snap them together, rather than writing long step-by-step procedures.
For a guided tour of functions and higher-order programming, see Learn You a Haskell, chapter on higher-order functions: http://learnyouahaskell.com/higher-order-functions
Pattern Matching and Algebraic Data Types
Model data with your own types and take it apart by shape.
Algebraic data types (ADTs) are how you describe data in Haskell, and pattern matching is how you take that data apart. Together they replace the tangle of classes, enums, and null checks found in many other languages.
You define a type with the data keyword. A simple enumeration lists its alternatives, called constructors, separated by |:
data Direction = North | South | East | West
Constructors can carry data. A Shape is either a circle with a radius or a rectangle with two sides:
data Shape
= Circle Double
| Rect Double Double
Here Circle and Rect are functions that build a Shape: Circle 2.0 and Rect 3.0 4.0 are both values of type Shape. The two halves, summing alternatives and pairing fields together, are why these are called algebraic types.
Pattern matching inspects which constructor a value used and binds its fields to names. The cleanest form writes one equation per case:
area :: Shape -> Double
area (Circle r) = pi * r * r
area (Rect w h) = w * h
GHC checks your patterns for exhaustiveness. If you forget a constructor, the compiler warns that a case is unhandled, so whole classes of "unhandled state" bugs disappear. You can also match inside a single expression with case:
name :: Direction -> String
name d = case d of
North -> "north"
South -> "south"
East -> "east"
West -> "west"
Patterns nest and can match literals, the empty list [], the cons pattern (x:xs) that splits a head from a tail, and the wildcard _ that matches anything:
sumList :: [Int] -> Int
sumList [] = 0
sumList (x:xs) = x + sumList xs
This recursion reads like a definition: the sum of an empty list is 0, and the sum of x followed by xs is x plus the sum of the rest.
Types can be polymorphic and recursive. Haskell's own Maybe type models an optional value and is the language's principled answer to null:
data Maybe a = Nothing | Just a
safeHead :: [a] -> Maybe a
safeHead [] = Nothing
safeHead (x:_) = Just x
A caller cannot ignore the absent case, because the result is a Maybe a, not an a, and pattern matching forces them to handle Nothing.
Record syntax names the fields and generates accessor functions automatically:
data Person = Person
{ name :: String
, age :: Int
}
alice :: Person
alice = Person { name = "Alice", age = 30 }
-- name alice == "Alice"; age alice == 30
Update a record without mutation by copying with changes:
older :: Person -> Person
older p = p { age = age p + 1 }
This produces a new Person and leaves the original untouched, which is the norm in a language where values are immutable. Define data with ADTs, then let pattern matching and the exhaustiveness checker keep your logic honest.
For the full grammar of data, patterns, and records, see the GHC users guide section on data types and the Haskell wikibook: https://en.wikibooks.org/wiki/Haskell/Pattern_matching
Type Classes
Define and implement interfaces shared across many types.
A type class is Haskell's mechanism for ad hoc polymorphism: a named set of operations that many different types can implement. It plays a role similar to interfaces or traits in other languages, but with extra power because the compiler chooses the implementation from the types alone.
You have already used classes implicitly. The signature (+) :: Num a => a -> a -> a says: for any type a that is an instance of Num, addition is defined. The part before => is a constraint. It restricts the type variable to types that provide the required operations.
Three classes you meet constantly:
Eq- supports equality with==and/=Ord- supports ordering with<,>,compareShow- can be rendered to aStringwithshow
ghci> show (1 + 2)
"3"
ghci> compare 3 5
LT
ghci> 'a' == 'b'
False
A constrained function works for every type satisfying the constraint. Here is largest, which works on any orderable, non-empty list:
largest :: Ord a => [a] -> a
largest = foldr1 max
It runs on [Int], [Char], [Double], or any other Ord type without change.
Defining your own class. Suppose you want a uniform way to describe values:
class Describable a where
describe :: a -> String
Now provide instances for concrete types, supplying the actual code:
data Pet = Dog | Cat
instance Describable Pet where
describe Dog = "a loyal dog"
describe Cat = "an aloof cat"
instance Describable Bool where
describe True = "yes"
describe False = "no"
Calling describe Dog selects the Pet instance; describe True selects the Bool instance. The compiler resolves which describe to run purely from the argument's type.
Classes can supply default methods, so an instance only needs to override what differs. Eq, for example, defines /= in terms of ==, which is why you usually implement just one of them.
Deriving spares you boilerplate. For most data types, GHC can write the obvious Eq, Ord, Show, Enum, and Bounded instances automatically:
data Color = Red | Green | Blue
deriving (Eq, Ord, Show, Enum, Bounded)
ghci> show Green
"Green"
ghci> Red < Blue
True
ghci> [minBound .. maxBound] :: [Color]
[Red,Green,Blue]
That single deriving clause generated equality, an ordering, a printable form, and enumeration, all consistent with the declared order of constructors.
Classes also enable superclasses, where one class requires another. Ord is declared with class Eq a => Ord a, meaning every orderable type must first be comparable for equality. This layering is how the standard library composes capabilities cleanly.
The payoff is reuse without inheritance hierarchies: you write a function once against a constraint like Num a or Ord a, and it instantly applies to every present and future type that satisfies it. The next lesson on monads builds directly on this idea, since Functor, Applicative, and Monad are themselves type classes.
A thorough introduction to classes and instances is in Learn You a Haskell: http://learnyouahaskell.com/types-and-typeclasses
Laziness and Evaluation
Understand non-strict evaluation, thunks, and infinite data.
Haskell is lazy, or more precisely non-strict: an expression is not evaluated until its result is actually needed. Until then it is held as an unevaluated promise called a thunk. This single property gives Haskell some of its most distinctive abilities and a few pitfalls worth knowing.
The simplest consequence: function arguments are only computed if used. Consider:
const3 :: a -> b -> a
const3 x _ = x
Calling const3 7 (error "boom") returns 7 and never triggers the error, because the second argument is never forced. In a strict language the error would blow up first.
Infinite data structures become ordinary values. You can define an endless list and take only the part you need:
nats :: [Integer]
nats = [0 ..] -- 0, 1, 2, 3, ... forever
ghci> take 5 nats
[0,1,2,3,4]
Laziness evaluates just enough of nats to produce five elements, then stops. The same trick defines self-referential streams. The Fibonacci sequence is a famous one-liner:
fibs :: [Integer]
fibs = 0 : 1 : zipWith (+) fibs (tail fibs)
ghci> take 8 fibs
[0,1,1,2,3,5,8,13]
Here fibs is defined in terms of itself, and laziness ties the knot so each element is produced on demand.
Laziness also enables clean separation of generation and consumption. You can write takeWhile (< 100) (map (^ 2) [1 ..]) and only the squares below 100 are ever computed, even though the source list is infinite:
ghci> takeWhile (< 100) (map (^ 2) [1 ..])
[1,4,9,16,25,36,49,64,81]
Weak head normal form (WHNF). When Haskell forces a value, it evaluates only to the outermost constructor, not all the way down. take 3 (1 : 2 : undefined) works because the third element is never reached. Understanding WHNF explains why some computations succeed despite containing undefined deeper inside.
The pitfall: space leaks. Thunks can pile up. A naive left fold that sums a huge list builds a tall chain of unevaluated additions before reducing any of them, which can exhaust memory:
import Data.List (foldl')
total :: [Int] -> Int
total = foldl' (+) 0 -- foldl' forces each step, staying constant in space
foldl' from Data.List is the strict left fold; prefer it over lazy foldl for accumulating over large lists.
When you do need strictness, force evaluation explicitly. The seq function evaluates its first argument to WHNF before returning the second, and the bang pattern !x (with the BangPatterns extension) makes a binding strict:
{-# LANGUAGE BangPatterns #-}
sumStrict :: [Int] -> Int
sumStrict = go 0
where
go !acc [] = acc
go !acc (x:xs) = go (acc + x) xs
The !acc forces the accumulator on every step, so no thunk chain accumulates. The mental model to carry away: by default Haskell computes nothing until forced, which buys you composability and infinite structures, and when performance demands it you add strictness in the few places that matter.
The GHC users guide and the Haskell wiki cover evaluation in depth: https://wiki.haskell.org/Lazy_evaluation
Monads and IO
Sequence effects and computations with do notation.
Because Haskell functions are pure, performing input and output, or threading a possibly-missing value through a computation, needs a structured approach. That structure is the monad, and it is what lets pure code describe effectful programs cleanly.
Start concretely with IO. A value of type IO a is a description of an effectful action that, when run, yields a value of type a. main has type IO (), an action producing nothing useful:
main :: IO ()
main = do
putStrLn "What is your name?"
name <- getLine
putStrLn ("Hello, " ++ name ++ "!")
The do block sequences actions top to bottom. The left arrow <- runs an action and binds its result: getLine :: IO String, so name is the String the user typed. Plain let works inside do for pure bindings, while <- is for extracting an action's result.
Under the hood, do is sugar for the bind operator >>=, whose type is the essence of a monad:
(>>=) :: Monad m => m a -> (a -> m b) -> m b
Read it as: take a computation producing an a, and a function that uses that a to produce the next computation, and chain them. The Monad class also provides return (also called pure), which wraps a plain value as a trivial computation. The earlier example desugars to:
main =
putStrLn "What is your name?" >>
getLine >>= \name ->
putStrLn ("Hello, " ++ name ++ "!")
where >> sequences two actions when the first result is ignored.
Monads are not only about IO. Maybe is a monad that models computations which may fail. Binding short-circuits on Nothing, so you chain fallible steps without nested case expressions:
import Text.Read (readMaybe)
addStrings :: String -> String -> Maybe Int
addStrings a b = do
x <- readMaybe a
y <- readMaybe b
return (x + y)
ghci> addStrings "3" "4"
Just 7
ghci> addStrings "3" "oops"
Nothing
If either readMaybe returns Nothing, the whole block is Nothing, with no explicit error handling written by you. The exact same do syntax that drove IO now drives failure handling, because both are monads.
Either e is the related monad for computations that fail with a reason. Left err carries an error value, Right x carries success, and binding propagates the first Left:
safeDiv :: Int -> Int -> Either String Int
safeDiv _ 0 = Left "divide by zero"
safeDiv x y = Right (x `div` y)
compute :: Either String Int
compute = do
a <- safeDiv 10 2
b <- safeDiv a 0 -- fails here
return (a + b) -- never reached; compute is Left "divide by zero"
Monads sit atop two simpler classes. Functor provides fmap, which maps a pure function over a wrapped value (fmap (+1) (Just 4) is Just 5). Applicative adds pure and <*> for applying wrapped functions to wrapped arguments. Every Monad is an Applicative, and every Applicative is a Functor, so the abstractions stack neatly.
The practical takeaway: do notation gives you familiar, imperative-looking sequencing, while the type system tracks exactly which effects each computation may have. An IO in the type means real-world effects; a Maybe or Either means it may fail. Purity is preserved because effects are values you build and combine, and only main actually runs them.
The canonical gentle introduction is the "functors, applicative functors and monoids" and "a fistful of monads" chapters of Learn You a Haskell: http://learnyouahaskell.com/a-fistful-of-monads
Ecosystem and Tooling
Hackage, Cabal, Stack, formatters, testing, and the wider language family.
A language is only as practical as its ecosystem, and Haskell's is mature and well organized. This lesson maps the libraries and tools you will reach for in real projects.
Hackage is the central package archive at hackage.haskell.org, hosting tens of thousands of libraries with searchable, auto-generated API documentation. Its companion Hoogle lets you search by type signature, not just by name. If you need a function of type (a -> b) -> [a] -> [b], Hoogle will point you straight at map. Searching by type is a uniquely Haskell superpower.
Build tools. Two coexist:
- Cabal is the standard tool. A
.cabalfile declares your package, its modules, executables, test suites, and dependency bounds. Everyday commands arecabal build,cabal run,cabal test, andcabal repl. - Stack layers reproducibility on top via curated resolver snapshots (for example
lts-22.x), a set of package versions guaranteed to build together.stack build,stack run, andstack testmirror the Cabal commands.
A minimal .cabal executable stanza looks like this:
executable myapp
main-is: Main.hs
build-depends: base >=4 && <5, text, containers
default-language: Haskell2010
Each name in build-depends is a Hackage package, resolved and compiled on the next build.
Workhorse libraries you will use again and again:
base- the standard library, always available (Prelude, lists, IO).textandbytestring- efficient string and binary types; preferTextoverStringfor performance.containers-Map,Set, andSeq.aeson- JSON encoding and decoding, the de facto standard.mtl- monad transformers for combining effects.QuickCheckandhspec- property-based and specification-style testing.
QuickCheck deserves a mention because it was pioneered in Haskell: instead of writing example cases, you state a property and the library generates hundreds of random inputs to try to falsify it:
import Test.QuickCheck
prop_reverseTwice :: [Int] -> Bool
prop_reverseTwice xs = reverse (reverse xs) == xs
main :: IO ()
main = quickCheck prop_reverseTwice
If the property ever fails, QuickCheck shrinks the counterexample to the smallest input that breaks it.
Code quality tooling:
- HLint suggests idiomatic improvements ("use
mapinstead of this fold"). - Ormolu and Fourmolu are automatic formatters that enforce a consistent style.
- Haskell Language Server (HLS) powers editor features: completions, type-on-hover, jump-to-definition, and refactors, installed through GHCup.
Language editions and extensions. The standardized language is defined by reports: Haskell 98 (1999, revised 2003) and Haskell 2010. Beyond the standard, GHC offers opt-in language extensions enabled with pragmas like {-# LANGUAGE OverloadedStrings #-} at the top of a file. Modern projects commonly enable a handful (such as OverloadedStrings and DerivingStrategies); the GHC2021 set bundles widely-used ones by default.
The wider family. Haskell's ideas spread widely. It directly influenced Rust (traits, algebraic types), Scala and F# (type classes, functional style), Elm and PureScript (pure functional front-end languages), and Idris (dependent types). Haskell itself descends from Miranda and the ML family, and is named after the logician Haskell Curry, whose work on combinatory logic underlies the whole tradition.
For production use, the standard advice is: use Cabal or Stack for builds, Text for strings, aeson for JSON, HLS in your editor, and HLint plus a formatter in CI. Start your dependency hunts on Hoogle.
The official package archive and Hoogle are the two links to bookmark: https://hackage.haskell.org/ and https://hoogle.haskell.org/