haskell.dev
Haskell, explained by the code you already write
Every idea here arrives three ways: the Haskell version, the Python version, and the JavaScript version. You already know what a loop, a dictionary and a try block do. This guide shows what happens to them when a compiler refuses to let you lie about types or hide a side effect.
- chapters
- 24
- runnable files
- 68
- dependencies to read it
- 0
Why bother with a compiler this strict
Haskell asks for something up front that Python and JavaScript let you skip: saying what everything is. Here is what you get in return.
You already know how to build things. The question is not whether you can write a web server or a script that renames files, because you can. The question is what changes when the language stops trusting you.
In Haskell a type signature is a contract the compiler enforces. It says what goes in, what comes out, and, unusually, whether the function is allowed to do anything else at all.
-- Three type signatures, three promises the compiler enforces.
-- Same name in, same greeting out, every single time. This function cannot
-- print, cannot read a file and cannot throw a surprise exception, because
-- none of that appears in its type.
greet :: String -> String
greet name = "Hello, " ++ name ++ "!"
-- The answer might be missing, and Maybe says so out loud. Every caller has
-- to decide what happens when the name is not there.
lookupAge :: String -> [(String, Int)] -> Maybe Int
lookupAge = lookup
-- IO in the type is the compiler's way of saying "this one touches the
-- outside world". Nothing without IO in its type is allowed to call it.
main :: IO ()
main = putStrLn (greet "world")
The same three functions in the languages you already use. The annotations are honest about intent and powerless about behaviour.
# The same three functions in Python. The annotations are documentation:
# nothing stops greet() from printing, opening a socket or raising.
def greet(name: str) -> str:
return f"Hello, {name}!"
def lookup_age(name: str, people: dict[str, int]) -> int | None:
return people.get(name)
def main() -> None:
print(greet("world"))
if __name__ == "__main__":
main()
// The same three functions in JavaScript. JSDoc types help an editor, and
// stop nobody: greet can print, fetch and throw without changing shape.
/** @param {string} name */
export function greet(name) {
return `Hello, ${name}!`;
}
/**
* @param {string} name
* @param {Record<string, number>} people
* @returns {number | undefined}
*/
export function lookupAge(name, people) {
return people[name];
}
console.log(greet("world"));
Nothing in either version stops the function from opening a file, mutating a global, or throwing.
The three promises
purity
Same input, same output
A function without IO in its type cannot read the clock, hit the network or print. Give it the same arguments tomorrow and it returns the same answer. That is why Haskell code is easy to test: most of it needs no mocks, no fixtures and no setup.
totality
Missing values are visible
There is no null and no undefined. A value that might be absent has type Maybe a, and the compiler will not let you use it as though it were always there. The class of bug that causes most production incidents simply does not typecheck.
immutability
Nothing changes behind your back
Values do not mutate. Passing a list to a function cannot alter the caller's copy, because there is no copy and no alteration. Once you stop tracking who might have changed what, whole categories of debugging disappear.
inference
You rarely have to say it twice
Types are inferred. Writing signatures on top level functions is a convention because they document intent, but inside a function the compiler works out the rest. You get the checking without the ceremony of Java.
What this guide does
Every idea gets a Haskell version and the Python or JavaScript version beside it, so you can see the shape of the translation rather than guessing at it. Roughly half the chapters build something runnable: a guessing game, a flashcard drill, a word counter, a program that tidies a downloads folder.
Read it in order if you are new to the language. If you have written some Haskell before, the sidebar is a menu; laziness, type classes and concurrency all stand on their own.
Install GHC and run something
One installer, one file, one prompt. Fifteen minutes from nothing to a working setup with editor support.
GHCup installs the compiler, the build tool and the language server together, and lets you switch versions later without reinstalling anything. Use it on macOS, Linux and WSL. On Windows there is a PowerShell one liner on the same page.
# One installer for the whole toolchain, on macOS, Linux and WSL.
curl --proto '=https' --tlsv1.2 -sSf https://get-ghcup.haskell.org | sh
# It sets up four things:
# ghc the compiler
# cabal the build tool and package manager
# hls the language server, for editor support
# stack an alternative build tool, optional
ghcup tui # pick and switch versions from a menu
ghc --version
cabal --version
-
Install the toolchain
Run the command above and accept the defaults. Say yes when it offers to install the Haskell Language Server, since that is what gives your editor types on hover and errors as you type.
-
Add the language server to your editor
In VS Code install the extension named Haskell. In Neovim point your LSP config at
haskell-language-server-wrapper. Both find the toolchain GHCup installed without further configuration. -
Check it worked
ghc --versionandcabal --versionshould both answer. If your shell cannot find them, open a new terminal so the updated PATH takes effect.
Your first file
-- Every runnable program exports main, and main has type IO ().
-- The () is the empty tuple, Haskell's "nothing useful to return".
module Main (main) where
main :: IO ()
main = do
putStrLn "Hello from GHC."
putStrLn (unwords (replicate 3 "lambda"))
# Start the interactive prompt. It is a calculator, a type inspector and a
# scratchpad, and it is where most Haskell learning actually happens.
ghci
# Run a single file without building a project.
runghc hello.hs
# Compile it to a binary instead.
ghc -O2 hello.hs -o hello
./hello
runghc compiles and runs in one step, which is what you want while learning. ghc -O2 produces a native binary with no runtime to install alongside it, which is what you want when you ship.
GHCi is where the learning happens
GHCi is the interactive prompt. It evaluates expressions, loads your files, and, most usefully, tells you the type of anything you ask about. :type is the single most valuable command in the language, and it is worth using constantly for the first month.
ghci> 2 + 2 * 10
22
ghci> :type "hello"
"hello" :: String
ghci> :type (++)
(++) :: [a] -> [a] -> [a]
ghci> let name = "ada"
ghci> "hi " ++ name
"hi ada"
ghci> :type map
map :: (a -> b) -> [a] -> [b]
ghci> map (* 2) [1, 2, 3]
[2,4,6]
ghci> :info Bool
type Bool :: *
data Bool = False | True
ghci> :load Lists.hs
[1 of 1] Compiling Lists
Ok, one module loaded.
ghci> :reload
ghci> :quit
The syntax table
The translations you will want on a second monitor for the first week. Everything in the Haskell column compiles.
Most of the strangeness in Haskell syntax comes from three decisions: function application is a space rather than parentheses, types are written after a double colon, and everything is an expression. The rest is vocabulary.
| Idea | Haskell | Python | JavaScript |
|---|---|---|---|
| Whole number | count :: Int | count: int | let count = 0 |
| Decimal | ratio :: Double | ratio: float | let ratio = 0.0 |
| Text | label :: String | label: str | let label = "" |
| Boolean | flag :: Bool | flag: bool | let flag = true |
| List | primes :: [Int] | primes: list[int] | const primes = [] |
| Tuple | point :: (Int, String) | point: tuple[int, str] | const point = [1, "one"] |
| Dictionary | Map.Map String Int | dict[str, int] | new Map() |
| Missing value | Maybe String | str | None | string | undefined |
| Failure | Either String Int | raise ValueError | throw new Error() |
| Function type | add :: Int -> Int -> Int | def add(x: int, y: int) -> int | function add(x, y) |
| Calling it | add 2 3 | add(2, 3) | add(2, 3) |
| Anonymous function | \x -> x * 2 | lambda x: x * 2 | (x) => x * 2 |
| Type alias | type Name = String | Name = str | type Name = string |
| New type | newtype Email = Email String | class Email(NamedTuple) | class Email {} |
Every line of the Haskell column above is taken from this file, which compiles as it stands.
-- Every declaration in the comparison table, in one compilable file.
module Rosetta where
import qualified Data.Map.Strict as Map
count :: Int
count = 42
ratio :: Double
ratio = 3.14
flag :: Bool
flag = True
label :: String
label = "hi"
primes :: [Int]
primes = [2, 3, 5, 7]
point :: (Int, String)
point = (1, "one")
ages :: Map.Map String Int
ages = Map.fromList [("ada", 36), ("grace", 45)]
-- No null and no undefined. A missing value has its own type.
middleName :: Maybe String
middleName = Nothing
-- No exceptions for expected failures. The error type is in the signature.
parsed :: Either String Int
parsed = Right 5
-- Functions are values, and their type is written with arrows.
add :: Int -> Int -> Int
add x y = x + y
-- A type synonym is a nickname, not a new type.
type Name = String
-- A newtype is a new type with no runtime cost, so Name and Email cannot
-- be swapped by accident.
newtype Email = Email String
Reading a signature out loud
add :: Int -> Int -> Int reads as "add takes an Int, then another Int, and gives back an Int". The arrows look odd until you learn why they are there: every function of two arguments is really a function of one argument that returns another function. That is what makes partial application ordinary rather than a trick, and it comes up again in the chapter on higher order functions.
A fat arrow means a constraint rather than an argument. sort :: Ord a => [a] -> [a] reads as "for any type a that can be ordered, sort takes a list of a and returns a list of a". Everything to the left of => is a requirement on the type variables, not a value you pass.
ghci> :type 42
42 :: Num a => a
ghci> :type (42 :: Int)
(42 :: Int) :: Int
ghci> :type words
words :: String -> [String]
ghci> :type filter even
filter even :: Integral a => [a] -> [a]
ghci> :type lookup "ada"
lookup "ada" :: [(String, b)] -> Maybe b
ghci> "one" + 1
error: No instance for (Num String) arising from a use of '+'
ghci> :type foldr
foldr :: Foldable t => (a -> b -> b) -> b -> t a -> b
The last one is a type error on purpose. Haskell will not add a number to a string, and it says so before the program runs.
Functions, guards and where
No return keyword, no parentheses around arguments, and two ways to name intermediate values. This is most of the syntax you need.
A definition is a signature followed by an equation. The body is the result, so there is nothing to return. Arguments are separated by spaces at the call site and by arrows in the type.
module Functions where
-- A signature, then an equation. No return keyword: the body is the result.
double :: Int -> Int
double x = x * 2
-- Arguments are separated by arrows, not commas, and applied with spaces.
area :: Double -> Double -> Double
area width height = width * height
-- Guards test conditions in order and pick the first that holds.
-- `otherwise` is not a keyword, it is just a name for True.
classify :: Int -> String
classify n
| n < 0 = "negative"
| n == 0 = "zero"
| n < 10 = "small"
| otherwise = "large"
-- `where` names intermediate values for the whole definition.
bmiTell :: Double -> Double -> String
bmiTell weight height
| bmi <= 18.5 = "underweight, bmi " ++ show bmi
| bmi <= 25.0 = "normal, bmi " ++ show bmi
| otherwise = "overweight, bmi " ++ show bmi
where
bmi = weight / height ^ (2 :: Int)
-- `let ... in` is an expression, so it can appear anywhere a value can.
cylinderArea :: Double -> Double -> Double
cylinderArea radius height =
let side = 2 * pi * radius * height
cap = pi * radius ^ (2 :: Int)
in side + 2 * cap
-- Any two argument function can be written between its arguments.
remainder :: Int -> Int -> Int
remainder a b = a `mod` b
-- Any operator can be written in front of them.
plus :: Int -> Int -> Int
plus = (+)
-- Anonymous functions start with a backslash, which is meant to look like
-- the lambda it stands for.
addOneToAll :: [Int] -> [Int]
addOneToAll = map (\n -> n + 1)
def double(x: int) -> int:
return x * 2
def area(width: float, height: float) -> float:
return width * height
def classify(n: int) -> str:
if n < 0:
return "negative"
if n == 0:
return "zero"
if n < 10:
return "small"
return "large"
def bmi_tell(weight: float, height: float) -> str:
bmi = weight / height**2
if bmi <= 18.5:
return f"underweight, bmi {bmi}"
if bmi <= 25.0:
return f"normal, bmi {bmi}"
return f"overweight, bmi {bmi}"
def cylinder_area(radius: float, height: float) -> float:
side = 2 * 3.141592653589793 * radius * height
cap = 3.141592653589793 * radius**2
return side + 2 * cap
def add_one_to_all(xs: list[int]) -> list[int]:
return [n + 1 for n in xs]
export const double = (x) => x * 2;
export const area = (width, height) => width * height;
export function classify(n) {
if (n < 0) return "negative";
if (n === 0) return "zero";
if (n < 10) return "small";
return "large";
}
export function bmiTell(weight, height) {
const bmi = weight / height ** 2;
if (bmi <= 18.5) return `underweight, bmi ${bmi}`;
if (bmi <= 25.0) return `normal, bmi ${bmi}`;
return `overweight, bmi ${bmi}`;
}
export function cylinderArea(radius, height) {
const side = 2 * Math.PI * radius * height;
const cap = Math.PI * radius ** 2;
return side + 2 * cap;
}
export const addOneToAll = (xs) => xs.map((n) => n + 1);
Guards instead of a chain of ifs
The vertical bars are guards. Each one is a condition, tested top to bottom, and the first that holds gives the result. otherwise looks like a keyword but is just a name bound to True, which is why it always matches.
Haskell has if then else as well, and it is an expression, so the else branch is not optional. There is no way to write an if that produces a value on one path and nothing on the other.
where and let
where attaches names to a whole definition, including across all its guards, which is why bmi above can be used in three branches without being computed three times. It goes at the bottom, so the interesting line stays at the top.
let ... in is an expression and can appear anywhere a value can, including inside a list comprehension or halfway through a do block. Use where for the definition as a whole and let for something local to one expression.
ghci> :type 42
42 :: Num a => a
ghci> :type (42 :: Int)
(42 :: Int) :: Int
ghci> :type words
words :: String -> [String]
ghci> :type filter even
filter even :: Integral a => [a] -> [a]
ghci> :type lookup "ada"
lookup "ada" :: [(String, b)] -> Maybe b
ghci> "one" + 1
error: No instance for (Num String) arising from a use of '+'
ghci> :type foldr
foldr :: Foldable t => (a -> b -> b) -> b -> t a -> b
Load a file with :load and every function in it becomes available for experiments.
Pattern matching
Write one equation per shape of input. The compiler tells you which shapes you forgot.
Pattern matching is the feature people miss most after leaving Haskell. Instead of taking a value apart with conditionals and indexing, you describe the shapes it might have and give an answer for each.
module Patterns where
-- One definition per shape of input. GHC warns when a case is missing.
describe :: [String] -> String
describe [] = "nothing"
describe [x] = "just " ++ x
describe [x, y] = x ++ " and " ++ y
describe (x : rest) = x ++ " and " ++ show (length rest) ++ " more"
-- `case` is the same idea written as an expression.
httpMessage :: Int -> String
httpMessage code = case code of
200 -> "OK"
404 -> "Not Found"
500 -> "Server Error"
_ -> "Status " ++ show code
-- Tuples come apart exactly the way Python unpacks them.
distance :: (Double, Double) -> (Double, Double) -> Double
distance (x1, y1) (x2, y2) = sqrt ((x2 - x1) ** 2 + (y2 - y1) ** 2)
-- An as-pattern keeps the whole value while still matching its shape.
stutter :: String -> String
stutter whole@(c : _) = c : whole
stutter [] = []
-- Guards can be attached to any pattern.
grade :: Int -> Char
grade score
| score >= 90 = 'A'
| score >= 80 = 'B'
| score >= 70 = 'C'
| otherwise = 'F'
-- Comparing returns one of three constructors instead of an integer, so
-- there is no "what does -1 mean again" moment.
signOf :: Int -> String
signOf n = case compare n 0 of
LT -> "negative"
EQ -> "zero"
GT -> "positive"
Python's match statement, added in 3.10, is close in spirit. JavaScript has destructuring but no matching, so the shapes are checked by hand.
def describe(items: list[str]) -> str:
match items:
case []:
return "nothing"
case [x]:
return f"just {x}"
case [x, y]:
return f"{x} and {y}"
case [x, *rest]:
return f"{x} and {len(rest)} more"
return "unreachable"
def http_message(code: int) -> str:
match code:
case 200:
return "OK"
case 404:
return "Not Found"
case 500:
return "Server Error"
case _:
return f"Status {code}"
def distance(a: tuple[float, float], b: tuple[float, float]) -> float:
(x1, y1), (x2, y2) = a, b
return ((x2 - x1) ** 2 + (y2 - y1) ** 2) ** 0.5
def grade(score: int) -> str:
if score >= 90:
return "A"
if score >= 80:
return "B"
if score >= 70:
return "C"
return "F"
// There is no pattern matching, so shapes are checked by hand.
export function describe(items) {
if (items.length === 0) return "nothing";
if (items.length === 1) return `just ${items[0]}`;
if (items.length === 2) return `${items[0]} and ${items[1]}`;
const [first, ...rest] = items;
return `${first} and ${rest.length} more`;
}
export function httpMessage(code) {
switch (code) {
case 200:
return "OK";
case 404:
return "Not Found";
case 500:
return "Server Error";
default:
return `Status ${code}`;
}
}
export function distance([x1, y1], [x2, y2]) {
return Math.hypot(x2 - x1, y2 - y1);
}
export function grade(score) {
if (score >= 90) return "A";
if (score >= 80) return "B";
if (score >= 70) return "C";
return "F";
}
The compiler checks that you covered everything
Build with -Wall and leaving out a case is a warning with the missing pattern printed for you. Most projects turn that warning into an error, which means the class of bug where a new enum value silently falls through a switch cannot reach production.
This gets more valuable as your types grow. Add a constructor to a data type and the compiler walks you through every place that now needs a new branch, which turns a refactor into a to do list rather than a search through the codebase.
Comparing returns a value, not a number
compare gives back LT, EQ or GT, so a three way comparison is a pattern match on three named constructors rather than a memory test about whether minus one meant smaller. The same trick shows up everywhere in Haskell: when a function has a small set of outcomes, they get a type.
Lists, ranges and comprehensions
Singly linked, lazy, and homogeneous. Once you know those three facts the rest of the list library follows.
A Haskell list is a linked list, not an array. Adding to the front is free, indexing into the middle is not, and the type says what it holds: [Int] is a list of Int and cannot contain a string in position four.
Lists are also lazy, which is why the range [1 ..] is a perfectly ordinary value rather than a hang. Nothing is produced until something asks for it.
module Lists where
import Data.Char (toUpper)
-- A list holds one type of thing, and the type says which.
primes :: [Int]
primes = [2, 3, 5, 7, 11]
-- (:) puts one element on the front. Lists are linked lists, so this is
-- cheap while appending to the end is not.
withOne :: [Int]
withOne = 1 : primes
-- Ranges. Two starting elements set the step.
countdown :: [Int]
countdown = [10, 9 .. 1]
evens :: [Int]
evens = [0, 2 .. 20]
letters :: String
letters = ['a' .. 'e']
-- Laziness makes an endless list an ordinary value, as long as something
-- eventually stops taking from it.
squares :: [Int]
squares = take 10 [n * n | n <- [1 ..]]
-- A comprehension reads like the set builder notation from a maths class:
-- outputs on the left, generators and filters on the right.
pythagorean :: [(Int, Int, Int)]
pythagorean =
[ (a, b, c)
| c <- [1 .. 20]
, b <- [1 .. c]
, a <- [1 .. b]
, a * a + b * b == c * c
]
-- A pattern in a generator quietly skips anything that does not match,
-- which is a safe way to take the first letter of each word.
initials :: String -> String
initials sentence = [c | (c : _) <- words sentence]
shout :: String -> String
shout = map toUpper
-- The list functions you will reach for daily. All of them return new
-- lists; none of them change the one you passed in.
summary :: [Int] -> (Int, Int, Int, [Int], [Int])
summary xs =
( length xs
, sum xs
, product xs
, take 3 xs
, reverse xs
)
from itertools import count, islice
primes = [2, 3, 5, 7, 11]
# Python lists are arrays, so prepending copies the whole thing.
with_one = [1, *primes]
countdown = list(range(10, 0, -1))
evens = list(range(0, 21, 2))
letters = [chr(c) for c in range(ord("a"), ord("f"))]
# There is no lazy list literal, so an infinite sequence needs itertools.
squares = [n * n for n in islice(count(1), 10)]
pythagorean = [
(a, b, c)
for c in range(1, 21)
for b in range(1, c + 1)
for a in range(1, b + 1)
if a * a + b * b == c * c
]
def initials(sentence: str) -> str:
return "".join(word[0] for word in sentence.split())
def summary(xs: list[int]) -> tuple[int, int, list[int], list[int]]:
total = sum(xs)
# Slicing and reversed() copy, but sort() and append() do not: some list
# methods return a new list and some change the one you have.
return (len(xs), total, xs[:3], list(reversed(xs)))
export const primes = [2, 3, 5, 7, 11];
export const withOne = [1, ...primes];
export const countdown = Array.from({ length: 10 }, (_, i) => 10 - i);
export const evens = Array.from({ length: 11 }, (_, i) => i * 2);
export const letters = Array.from({ length: 5 }, (_, i) =>
String.fromCharCode(97 + i),
);
// Nothing is lazy, so an endless sequence needs a generator.
function* naturals() {
for (let n = 1; ; n++) yield n;
}
export const squares = [];
for (const n of naturals()) {
if (squares.length === 10) break;
squares.push(n * n);
}
export const pythagorean = [];
for (let c = 1; c <= 20; c++) {
for (let b = 1; b <= c; b++) {
for (let a = 1; a <= b; a++) {
if (a * a + b * b === c * c) pythagorean.push([a, b, c]);
}
}
}
export const initials = (sentence) =>
sentence
.split(/\s+/)
.filter(Boolean)
.map((word) => word[0])
.join("");
// sort() and reverse() change the array in place and also return it, which
// is the source of a great many surprises.
export const summary = (xs) => [xs.length, xs.reduce((a, b) => a + b, 0), xs.slice(0, 3), [...xs].reverse()];
Both need a generator to express an endless sequence. In Haskell laziness is the default, so no extra machinery appears in the code.
Comprehensions
The syntax is the one from a maths textbook, and Python borrowed it from the same place: outputs on the left of the bar, generators and filters on the right. Multiple generators nest, with the rightmost varying fastest, exactly like nested loops.
ghci> take 5 [1 ..]
[1,2,3,4,5]
ghci> [x * x | x <- [1 .. 10], even x]
[4,16,36,64,100]
ghci> zip "abc" [1, 2, 3]
[('a',1),('b',2),('c',3)]
ghci> words "the quick brown fox"
["the","quick","brown","fox"]
ghci> unwords (reverse (words "the quick brown fox"))
"fox brown quick the"
ghci> splitAt 3 [1 .. 6]
([1,2,3],[4,5,6])
ghci> takeWhile (< 20) (map (* 3) [1 ..])
[3,6,9,12,15,18]
Try it yourself
Write initials, which turns "ada lovelace king" into "alk". Handle extra spaces without crashing, and do it without using head.
Hint: A pattern inside a comprehension generator skips anything that does not match.
Show one solution Hide the solution
module Lists where
import Data.Char (toUpper)
-- A list holds one type of thing, and the type says which.
primes :: [Int]
primes = [2, 3, 5, 7, 11]
-- (:) puts one element on the front. Lists are linked lists, so this is
-- cheap while appending to the end is not.
withOne :: [Int]
withOne = 1 : primes
-- Ranges. Two starting elements set the step.
countdown :: [Int]
countdown = [10, 9 .. 1]
evens :: [Int]
evens = [0, 2 .. 20]
letters :: String
letters = ['a' .. 'e']
-- Laziness makes an endless list an ordinary value, as long as something
-- eventually stops taking from it.
squares :: [Int]
squares = take 10 [n * n | n <- [1 ..]]
-- A comprehension reads like the set builder notation from a maths class:
-- outputs on the left, generators and filters on the right.
pythagorean :: [(Int, Int, Int)]
pythagorean =
[ (a, b, c)
| c <- [1 .. 20]
, b <- [1 .. c]
, a <- [1 .. b]
, a * a + b * b == c * c
]
-- A pattern in a generator quietly skips anything that does not match,
-- which is a safe way to take the first letter of each word.
initials :: String -> String
initials sentence = [c | (c : _) <- words sentence]
shout :: String -> String
shout = map toUpper
-- The list functions you will reach for daily. All of them return new
-- lists; none of them change the one you passed in.
summary :: [Int] -> (Int, Int, Int, [Int], [Int])
summary xs =
( length xs
, sum xs
, product xs
, take 3 xs
, reverse xs
)
Recursion instead of loops
There is no for and no while. A loop counter is a variable being reassigned, and Haskell has no reassignment, so loops become functions that call themselves.
This is the change that feels largest and turns out to be smallest. A loop has three parts: a starting state, a rule for the next state, and a stopping condition. A recursive function has the same three parts, written as a base case and a case that shrinks the problem.
module Recursion where
-- The base case first, then the case that shrinks the problem.
factorial :: Integer -> Integer
factorial 0 = 1
factorial n = n * factorial (n - 1)
-- Adding up a list, spelled out. `sum` does this for you.
mySum :: Num a => [a] -> a
mySum [] = 0
mySum (x : xs) = x + mySum xs
-- An accumulator argument replaces the variable you would have mutated.
-- This one is tail recursive, so GHC turns it into a loop.
myReverse :: [a] -> [a]
myReverse = go []
where
go acc [] = acc
go acc (x : xs) = go (x : acc) xs
-- Quicksort, the example every Haskell book opens with, and fairly so:
-- it is the definition of the algorithm rather than an implementation.
quickSort :: Ord a => [a] -> [a]
quickSort [] = []
quickSort (pivot : rest) = quickSort smaller ++ [pivot] ++ quickSort larger
where
smaller = [x | x <- rest, x <= pivot]
larger = [x | x <- rest, x > pivot]
-- A list that refers to itself. Laziness means only the part you look at
-- is ever built.
fibs :: [Integer]
fibs = 0 : rest
where
rest = 1 : zipWith (+) fibs rest
collatz :: Int -> [Int]
collatz 1 = [1]
collatz n
| even n = n : collatz (n `div` 2)
| otherwise = n : collatz (3 * n + 1)
-- Mutual recursion is allowed, and order of definition never matters.
isEven :: Int -> Bool
isEven 0 = True
isEven n = isOdd (n - 1)
isOdd :: Int -> Bool
isOdd 0 = False
isOdd n = isEven (n - 1)
from functools import lru_cache
from itertools import islice
def factorial(n: int) -> int:
return 1 if n == 0 else n * factorial(n - 1)
def my_sum(xs: list[int]) -> int:
# Recursion over a list of more than about a thousand items hits
# RecursionError, so real Python code writes the loop instead.
if not xs:
return 0
return xs[0] + my_sum(xs[1:])
def my_reverse(xs: list[int]) -> list[int]:
result: list[int] = []
for x in xs:
result.insert(0, x)
return result
def quick_sort(xs: list[int]) -> list[int]:
if not xs:
return []
pivot, rest = xs[0], xs[1:]
smaller = [x for x in rest if x <= pivot]
larger = [x for x in rest if x > pivot]
return quick_sort(smaller) + [pivot] + quick_sort(larger)
def fibs():
a, b = 0, 1
while True:
yield a
a, b = b, a + b
first_ten_fibs = list(islice(fibs(), 10))
@lru_cache(maxsize=None)
def collatz_length(n: int) -> int:
if n == 1:
return 1
return 1 + collatz_length(n // 2 if n % 2 == 0 else 3 * n + 1)
export const factorial = (n) => (n === 0 ? 1n : BigInt(n) * factorial(n - 1));
export function mySum(xs) {
// No tail call optimisation in practice, so deep recursion overflows the
// stack and real code writes a loop.
if (xs.length === 0) return 0;
const [first, ...rest] = xs;
return first + mySum(rest);
}
export function myReverse(xs) {
let result = [];
for (const x of xs) result = [x, ...result];
return result;
}
export function quickSort(xs) {
if (xs.length === 0) return [];
const [pivot, ...rest] = xs;
const smaller = rest.filter((x) => x <= pivot);
const larger = rest.filter((x) => x > pivot);
return [...quickSort(smaller), pivot, ...quickSort(larger)];
}
export function* fibs() {
let [a, b] = [0, 1];
for (;;) {
yield a;
[a, b] = [b, a + b];
}
}
export function collatz(n) {
const path = [n];
while (n !== 1) {
n = n % 2 === 0 ? n / 2 : 3 * n + 1;
path.push(n);
}
return path;
}
Both hit a stack limit long before the data does, which is why the idiomatic versions use loops. GHC compiles tail recursion into a jump, so the Haskell version has no such ceiling.
Accumulators replace mutable variables
myReverse above carries the answer so far as an argument. That is the general translation for any loop with a running total, a counter or a builder: whatever you would have mutated becomes a parameter, and each call passes the updated value along.
When the recursive call is the last thing a function does, GHC turns it into a jump with no stack growth. That is called a tail call, and it is why an accumulator version handles a list of ten million items without complaint.
Quicksort in five lines
The sort in the file above is the classic example, and it is worth reading closely. It says: an empty list is already sorted, and any other list is everything smaller than the first element, sorted, then that element, then everything larger, sorted. That is the definition of the algorithm rather than an implementation of it.
Self referencing values
fibs in the file is a list defined in terms of itself. Laziness makes this work: the definition describes how to produce the next element from the ones already produced, and elements are only produced when something asks. take 10 fibs builds exactly ten. Nothing else in mainstream programming looks quite like this.
Higher order functions and folds
map and filter you already use. Folds are the general case behind both of them, and currying is why partial application needs no special syntax.
Functions are values. They can be arguments, results, and elements of a list, and none of that requires a keyword. The three you will reach for every day are map, filter and a fold.
module HigherOrder where
import Data.Char (toLower, toUpper)
import Data.List (foldl', sortOn)
names :: [String]
names = ["ada", "grace", "alan", "barbara"]
-- Functions are ordinary values, so they can be passed in like any other.
shouted :: [String]
shouted = map (map toUpper) names
shortNames :: [String]
shortNames = filter ((<= 4) . length) names
-- foldr rebuilds a list with your own operator in place of (:) and your own
-- value in place of []. sum is foldr (+) 0, and so is almost everything else.
total :: [Int] -> Int
total = foldr (+) 0
longest :: [String] -> String
longest = foldr keepLonger ""
where
keepLonger candidate best
| length candidate > length best = candidate
| otherwise = best
-- foldl' is the strict left fold. Reach for it when folding a long list of
-- numbers, because it does not build a tower of pending additions.
average :: [Double] -> Double
average xs = added / count
where
(added, count) = foldl' step (0, 0) xs
step (runningTotal, seen) x = (runningTotal + x, seen + 1)
-- Every function of two arguments is a function returning a function, so
-- supplying one argument early is normal rather than clever.
addTen :: Int -> Int
addTen = (+ 10)
tenPercentOff :: [Double] -> [Double]
tenPercentOff = map (* 0.9)
-- Composition builds a pipeline that runs right to left.
slug :: String -> String
slug = map dashSpaces . map toLower
where
dashSpaces ' ' = '-'
dashSpaces c = c
-- ($) applies a function to everything on its right, which removes the
-- pile of closing parentheses.
loudLengths :: [String] -> String
loudLengths xs = unwords $ map show $ map length xs
byLength :: [String] -> [String]
byLength = sortOn length
-- zipWith walks two lists together and stops at the shorter one.
runningPairs :: [Int] -> [Int]
runningPairs xs = zipWith (+) xs (drop 1 xs)
from functools import reduce
names = ["ada", "grace", "alan", "barbara"]
shouted = [name.upper() for name in names]
short_names = [name for name in names if len(name) <= 4]
def total(xs: list[int]) -> int:
return reduce(lambda acc, x: acc + x, xs, 0)
def longest(xs: list[str]) -> str:
return max(xs, key=len, default="")
def average(xs: list[float]) -> float:
return sum(xs) / len(xs) if xs else 0.0
def add_ten(n: int) -> int:
return n + 10
def ten_percent_off(prices: list[float]) -> list[float]:
return [price * 0.9 for price in prices]
def slug(text: str) -> str:
return text.lower().replace(" ", "-")
def by_length(xs: list[str]) -> list[str]:
return sorted(xs, key=len)
def running_pairs(xs: list[int]) -> list[int]:
return [a + b for a, b in zip(xs, xs[1:])]
export const names = ["ada", "grace", "alan", "barbara"];
export const shouted = names.map((name) => name.toUpperCase());
export const shortNames = names.filter((name) => name.length <= 4);
export const total = (xs) => xs.reduce((acc, x) => acc + x, 0);
export const longest = (xs) =>
xs.reduce((best, candidate) => (candidate.length > best.length ? candidate : best), "");
export const average = (xs) => (xs.length === 0 ? 0 : total(xs) / xs.length);
export const addTen = (n) => n + 10;
export const tenPercentOff = (prices) => prices.map((price) => price * 0.9);
export const slug = (text) => text.toLowerCase().replaceAll(" ", "-");
// Comparator sorting mutates, so copy first.
export const byLength = (xs) => [...xs].sort((a, b) => a.length - b.length);
export const runningPairs = (xs) => xs.slice(0, -1).map((x, i) => x + xs[i + 1]);
Folds are the pattern behind all of them
A list is built from two things: (:) to add an element and [] for the empty case. foldr rebuilds a list with your own operator in place of (:) and your own value in place of []. That single idea covers a surprising amount of the standard library.
| Written as a fold | Familiar name |
|---|---|
| foldr (+) 0 | sum |
| foldr (*) 1 | product |
| foldr (:) [] | id, for lists |
| foldr (\x acc -> f x : acc) [] | map f |
| foldr (\x acc -> if p x then x : acc else acc) [] | filter p |
| foldr (\_ n -> n + 1) 0 | length |
| foldr (||) False | or |
ghci> foldr (+) 0 [1, 2, 3, 4]
10
ghci> foldr (:) [] [1, 2, 3]
[1,2,3]
ghci> foldr (\x acc -> show x ++ "," ++ acc) "" [1, 2, 3]
"1,2,3,"
ghci> foldl (-) 0 [1, 2, 3]
-6
ghci> foldr (-) 0 [1, 2, 3]
2
ghci> :type foldMap
foldMap :: (Foldable t, Monoid m) => (a -> m) -> t a -> m
ghci> sum [1 .. 100]
5050
Currying, sections and composition
add :: Int -> Int -> Int is a function that takes an Int and returns a function that takes an Int. Applying it to one argument is therefore not a special feature, it is just what happens. (+ 10) and (<= 4) are sections: an operator with one side filled in.
. composes two functions into one that runs right to left, and $ applies a function to everything on its right, which exists mostly to delete closing parentheses. Both are ordinary operators defined in the Prelude, not syntax.
Maybe, Either and the end of null
The billion dollar mistake is missing from the language. A value that might not be there has a type that says so.
There is no null, no None and no undefined in Haskell. When a function might not have an answer, it says so in its return type, and the compiler makes every caller deal with both possibilities.
module SafeValues where
import Data.Maybe (fromMaybe, mapMaybe)
import Text.Read (readMaybe)
people :: [(String, Int)]
people = [("ada", 36), ("grace", 45)]
-- lookup cannot find every key, so its type admits that.
ageOf :: String -> Maybe Int
ageOf name = lookup name people
-- Pattern matching forces both cases into the open. There is no way to
-- forget the missing one, because the code will not compile.
describeAge :: String -> String
describeAge name = case ageOf name of
Nothing -> name ++ " is not on the list"
Just age -> name ++ " is " ++ show age
-- fromMaybe supplies a fallback in one expression.
ageOrZero :: String -> Int
ageOrZero name = fromMaybe 0 (ageOf name)
-- Maybe is a Functor, so a pure function can be applied inside it.
nextBirthday :: String -> Maybe Int
nextBirthday name = fmap (+ 1) (ageOf name)
-- mapMaybe keeps the successes and drops the failures in one pass.
knownAges :: [String] -> [Int]
knownAges = mapMaybe ageOf
-- Either carries a reason for the failure instead of just its fact.
data AgeError
= NotANumber String
| OutOfRange Int
deriving (Show, Eq)
parseAge :: String -> Either AgeError Int
parseAge raw = case readMaybe raw of
Nothing -> Left (NotANumber raw)
Just n
| n < 0 || n > 130 -> Left (OutOfRange n)
| otherwise -> Right n
-- In a do block over Either, the first Left stops everything after it and
-- becomes the result. This is early return without the return statement.
parseCouple :: String -> String -> Either AgeError (Int, Int)
parseCouple first second = do
a <- parseAge first
b <- parseAge second
pure (a, b)
-- Turning one into the other is a normal function call.
toMaybe :: Either AgeError Int -> Maybe Int
toMaybe = either (const Nothing) Just
people = {"ada": 36, "grace": 45}
def age_of(name: str) -> int | None:
# .get returns None for a missing key, and nothing forces the caller to
# check. people[name] raises instead, and nothing says so in the type.
return people.get(name)
def describe_age(name: str) -> str:
age = age_of(name)
if age is None:
return f"{name} is not on the list"
return f"{name} is {age}"
def age_or_zero(name: str) -> int:
return age_of(name) or 0
def next_birthday(name: str) -> int | None:
age = age_of(name)
return None if age is None else age + 1
def known_ages(names: list[str]) -> list[int]:
return [age for age in map(age_of, names) if age is not None]
class AgeError(ValueError):
"""Raised for input that is not a usable age."""
def parse_age(raw: str) -> int:
# The failure is invisible in the signature. Callers find out at runtime,
# usually in production.
try:
n = int(raw)
except ValueError as problem:
raise AgeError(f"not a number: {raw}") from problem
if n < 0 or n > 130:
raise AgeError(f"out of range: {n}")
return n
const people = { ada: 36, grace: 45 };
// undefined for a missing key, and no warning if you forget to check.
export const ageOf = (name) => people[name];
export function describeAge(name) {
const age = ageOf(name);
if (age === undefined) return `${name} is not on the list`;
return `${name} is ${age}`;
}
export const ageOrZero = (name) => ageOf(name) ?? 0;
// Optional chaining is fmap for one specific structure, built into the
// syntax. Haskell gets the same behaviour from an ordinary function.
export const nextBirthday = (name) => {
const age = ageOf(name);
return age === undefined ? undefined : age + 1;
};
export const knownAges = (names) =>
names.map(ageOf).filter((age) => age !== undefined);
export class AgeError extends Error {}
export function parseAge(raw) {
// Throwing is invisible from the outside. Nothing in the signature says
// this can fail, and nothing makes the caller catch it.
const n = Number(raw);
if (!Number.isInteger(n)) throw new AgeError(`not a number: ${raw}`);
if (n < 0 || n > 130) throw new AgeError(`out of range: ${n}`);
return n;
}
In both versions the failure is invisible from the outside. Nothing in the signature says parse_age can raise, and nothing forces a caller to handle it.
Maybe when there is nothing to say, Either when there is
Maybe a has two values: Nothing, or Just x. Use it when absence needs no explanation, like a key that is not in a map.
Either e a has Left e for the failure and Right a for the success. Use it when the caller will want to know what went wrong. Making the error its own data type, rather than a string, means the compiler can check that you handled each kind of failure.
ghci> lookup "ada" [("ada", 36), ("grace", 45)]
Just 36
ghci> lookup "alan" [("ada", 36), ("grace", 45)]
Nothing
ghci> fmap (+ 1) (Just 36)
Just 37
ghci> fmap (+ 1) Nothing
Nothing
ghci> (+) <$> Just 1 <*> Just 2
Just 3
ghci> (+) <$> Just 1 <*> Nothing
Nothing
ghci> Just 36 >>= \age -> if age > 18 then Just "adult" else Nothing
Just "adult"
ghci> sequence [Just 1, Just 2, Just 3]
Just [1,2,3]
ghci> sequence [Just 1, Nothing, Just 3]
Nothing
You do not unwrap it every time
The reflex from other languages is to check for the missing case immediately and get back to normal values. Haskell encourages the opposite: keep working inside Maybe and unwrap once at the edge.
fmap applies a function inside it. <*> combines several. >>= chains steps that can each fail. traverse turns a list of results into a result of a list. fromMaybe supplies a default at the end. The chapter on Functor and Monad is about exactly these operators, and they work the same way on Either, on lists and on IO.
Try it yourself
Write parseAge so that "42" gives a number, "abc" gives a clear failure, and "999" gives a different failure. Then write parseCouple, which parses two of them and stops at the first problem.
Hint: Either is a Monad, so a do block gives you early return for free.
Show one solution Hide the solution
module SafeValues where
import Data.Maybe (fromMaybe, mapMaybe)
import Text.Read (readMaybe)
people :: [(String, Int)]
people = [("ada", 36), ("grace", 45)]
-- lookup cannot find every key, so its type admits that.
ageOf :: String -> Maybe Int
ageOf name = lookup name people
-- Pattern matching forces both cases into the open. There is no way to
-- forget the missing one, because the code will not compile.
describeAge :: String -> String
describeAge name = case ageOf name of
Nothing -> name ++ " is not on the list"
Just age -> name ++ " is " ++ show age
-- fromMaybe supplies a fallback in one expression.
ageOrZero :: String -> Int
ageOrZero name = fromMaybe 0 (ageOf name)
-- Maybe is a Functor, so a pure function can be applied inside it.
nextBirthday :: String -> Maybe Int
nextBirthday name = fmap (+ 1) (ageOf name)
-- mapMaybe keeps the successes and drops the failures in one pass.
knownAges :: [String] -> [Int]
knownAges = mapMaybe ageOf
-- Either carries a reason for the failure instead of just its fact.
data AgeError
= NotANumber String
| OutOfRange Int
deriving (Show, Eq)
parseAge :: String -> Either AgeError Int
parseAge raw = case readMaybe raw of
Nothing -> Left (NotANumber raw)
Just n
| n < 0 || n > 130 -> Left (OutOfRange n)
| otherwise -> Right n
-- In a do block over Either, the first Left stops everything after it and
-- becomes the result. This is early return without the return statement.
parseCouple :: String -> String -> Either AgeError (Int, Int)
parseCouple first second = do
a <- parseAge first
b <- parseAge second
pure (a, b)
-- Turning one into the other is a normal function call.
toMaybe :: Either AgeError Int -> Maybe Int
toMaybe = either (const Nothing) Just
Making your own types
Enumerations, records and containers all come from one declaration form. This is the feature that changes how you design programs.
A data declaration lists the ways a value of that type can be built. Read | as "or". Each constructor can carry as many values as it likes, which covers everything from a plain enumeration to a binary tree.
module Shapes where
-- An enumeration is a type whose values are listed by hand.
data Direction = North | South | East | West
deriving (Show, Eq, Enum, Bounded)
allDirections :: [Direction]
allDirections = [minBound .. maxBound]
-- Constructors can carry values. Read the | as "or".
data Shape
= Circle Double
| Rectangle Double Double
| Triangle Double Double Double
deriving (Show, Eq)
-- One equation per constructor, and GHC checks none are missing.
area :: Shape -> Double
area (Circle r) = pi * r * r
area (Rectangle w h) = w * h
area (Triangle a b c) = sqrt (s * (s - a) * (s - b) * (s - c))
where
s = (a + b + c) / 2
-- Record syntax names the fields and defines the accessors for you.
data Employee = Employee
{ employeeName :: String
, employeeEmail :: String
, employeeSalary :: Int
}
deriving (Show, Eq)
ada :: Employee
ada = Employee
{ employeeName = "Ada"
, employeeEmail = "ada@example.com"
, employeeSalary = 100000
}
-- Record update makes a copy with some fields changed. The original value
-- is untouched, so nothing else in the program can be surprised by it.
raise :: Int -> Employee -> Employee
raise amount employee =
employee { employeeSalary = employeeSalary employee + amount }
-- A type parameter makes the container work for any element type, the same
-- way list[T] does in Python.
data Tree a
= Leaf
| Node (Tree a) a (Tree a)
insert :: Ord a => a -> Tree a -> Tree a
insert x Leaf = Node Leaf x Leaf
insert x node@(Node left value right)
| x < value = Node (insert x left) value right
| x > value = Node left value (insert x right)
| otherwise = node
toList :: Tree a -> [a]
toList Leaf = []
toList (Node left x right) = toList left ++ [x] ++ toList right
fromList :: Ord a => [a] -> Tree a
fromList = foldr insert Leaf
from dataclasses import dataclass, replace
from enum import Enum, auto
from math import pi, sqrt
class Direction(Enum):
NORTH = auto()
SOUTH = auto()
EAST = auto()
WEST = auto()
@dataclass(frozen=True)
class Circle:
radius: float
@dataclass(frozen=True)
class Rectangle:
width: float
height: float
@dataclass(frozen=True)
class Triangle:
a: float
b: float
c: float
Shape = Circle | Rectangle | Triangle
def area(shape: Shape) -> float:
# A missing branch is a runtime surprise unless a type checker is run
# separately. GHC refuses to build the program.
match shape:
case Circle(radius):
return pi * radius * radius
case Rectangle(width, height):
return width * height
case Triangle(a, b, c):
s = (a + b + c) / 2
return sqrt(s * (s - a) * (s - b) * (s - c))
raise ValueError(f"unknown shape: {shape}")
@dataclass(frozen=True)
class Employee:
name: str
email: str
salary: int
def raise_salary(amount: int, employee: Employee) -> Employee:
# frozen=True plus replace() is the closest Python gets to record update.
return replace(employee, salary=employee.salary + amount)
export const Direction = Object.freeze({
North: "North",
South: "South",
East: "East",
West: "West",
});
// A tag field stands in for a real sum type.
export const circle = (radius) => ({ kind: "circle", radius });
export const rectangle = (width, height) => ({ kind: "rectangle", width, height });
export const triangle = (a, b, c) => ({ kind: "triangle", a, b, c });
export function area(shape) {
switch (shape.kind) {
case "circle":
return Math.PI * shape.radius ** 2;
case "rectangle":
return shape.width * shape.height;
case "triangle": {
const s = (shape.a + shape.b + shape.c) / 2;
return Math.sqrt(s * (s - shape.a) * (s - shape.b) * (s - shape.c));
}
// Forget a case and the answer is quietly undefined.
default:
throw new Error(`unknown shape: ${shape.kind}`);
}
}
// Spread copies one level. Anything nested is still shared with the
// original, which is where "why did that change" bugs come from.
export const raiseSalary = (amount, employee) => ({
...employee,
salary: employee.salary + amount,
});
Python gets close with frozen dataclasses and a union type. JavaScript needs a tag field and a convention that everyone remembers to follow.
Make the illegal states impossible
This is the design idea worth taking away, even if you never write Haskell professionally. Instead of a struct with optional fields and a comment explaining which combinations are valid, define a type where the invalid combinations cannot be written down.
A user that is either logged out, or logged in with a session token, is two constructors rather than one record with a nullable token. A request that is pending, succeeded with a body, or failed with an error, is three constructors rather than three optional fields. The compiler then keeps every branch honest for you.
records
Fields with names
Record syntax defines accessor functions automatically, so employeeSalary ada works with no boilerplate. Record update, employee { employeeSalary = 1 }, copies the value with one field changed and leaves the original alone.
parameters
Types that hold other types
data Tree a works for any element type, the way list[T] does in Python. The parameter appears on the left of the equals sign and can be used on the right.
deriving
Free instances
deriving (Show, Eq, Ord) writes the obvious printing, equality and ordering code for you. Ordering follows the order the constructors are declared in, which is worth remembering when it decides how your data sorts.
newtype
A wrapper with no cost
newtype Email = Email String makes a type the compiler keeps separate from String while compiling away to nothing at runtime. It is the cheapest way to stop yourself passing a user id where an order id was wanted.
Type classes
Overloading with rules attached. Closer to a Python protocol or a Rust trait than to a class, and the reason sort works on your own types.
A type class declares operations a type may support. An instance says how one particular type supports them. Nothing is inherited, nothing is constructed, and no data belongs to the class itself.
module Classes where
import Data.Char (toUpper)
import Data.List (sort)
data Animal = Dog | Cat | Parrot
deriving (Show, Eq, Ord, Enum, Bounded)
-- A class lists operations a type may support. It is closer to a Python
-- protocol than to a class, because it defines no data of its own.
class Describable a where
describe :: a -> String
-- A default lets an instance override only what it cares about.
loud :: a -> String
loud x = map toUpper (describe x) ++ "!"
instance Describable Animal where
describe Dog = "a dog"
describe Cat = "a cat"
describe Parrot = "a parrot"
instance Describable Bool where
describe True = "yes"
describe False = "no"
loud b = describe b ++ ", definitely"
-- A constraint before => reads "for any type a that is Describable".
introduce :: Describable a => [a] -> String
introduce = unwords . map describe
-- Deriving Eq and Ord writes the obvious instances, and every function in
-- the library that needs them starts working.
sorted :: [Animal]
sorted = sort [Parrot, Dog, Cat]
-- A newtype with hand written instances, for the cases where the obvious
-- rules are the wrong ones.
newtype CaseInsensitive = CaseInsensitive String
instance Eq CaseInsensitive where
CaseInsensitive a == CaseInsensitive b =
map toUpper a == map toUpper b
instance Show CaseInsensitive where
show (CaseInsensitive original) = original
sameName :: Bool
sameName = CaseInsensitive "Ada" == CaseInsensitive "ADA"
-- Semigroup and Monoid describe "combinable" and "combinable with an
-- identity". Strings, lists and maps are all instances.
newtype Score = Score Int
deriving (Show, Eq)
instance Semigroup Score where
Score a <> Score b = Score (a + b)
instance Monoid Score where
mempty = Score 0
totalScore :: [Score] -> Score
totalScore = mconcat
Constraints are requirements, not arguments
In introduce :: Describable a => [a] -> String, everything before the fat arrow is a requirement on the type variable. It reads "for any type a that has a Describable instance". At the call site the compiler works out which instance to use and passes it along invisibly, so there is no runtime lookup of the kind a virtual method table does.
ghci> :info Eq
type Eq :: * -> Constraint
class Eq a where
(==) :: a -> a -> Bool
(/=) :: a -> a -> Bool
ghci> :type (==)
(==) :: Eq a => a -> a -> Bool
ghci> maximum "haskell"
's'
ghci> compare 3 5
LT
ghci> succ 'a'
'b'
ghci> [minBound .. maxBound] :: [Bool]
[False,True]
ghci> show (Just [1, 2, 3])
"Just [1,2,3]"
ghci> read "42" :: Int
42
The ones you will meet in week one
| Class | What it gives you | Typical member |
|---|---|---|
| Eq | equality | ==, /= |
| Ord | ordering, and therefore sorting | compare, <, max |
| Show | a String for debugging | show |
| Read | parsing back from a String | readMaybe |
| Num | arithmetic | +, *, abs |
| Enum | successors and ranges | succ, [a .. b] |
| Bounded | smallest and largest value | minBound, maxBound |
| Semigroup | combining two values | <> |
| Monoid | combining, with an empty value | mempty, mconcat |
| Functor | mapping inside a structure | fmap, <$> |
| Foldable | folding any container | sum, length, toList |
| Traversable | effects across a structure | traverse, sequence |
Functor, Applicative and Monad
Three type classes, one idea: working with a value inside a structure without taking it out. No metaphors about burritos.
These names put people off, and they should not. Each one is a class with one or two operations, and you have used all of them in other languages without a name attached. Optional chaining is Functor. Promise.all is Applicative. await is Monad.
- fmap, <$>
- Apply an ordinary function to whatever is inside, leaving the structure as it was. On Maybe it skips Nothing. On a list it is map. On IO it transforms the eventual result.
- <*>
- The function is inside the structure too, so several independent values can be combined. If any of them is missing, so is the answer, and no branching is written by hand.
- >>=
- Chain steps where each one depends on the result of the last, and each one can fail or produce effects. Pronounced bind.
- do
- Syntax that turns a chain of binds into something that reads like statements. It is not a special mode: every do block is a stack of >>= calls.
module Effects where
import Data.Char (toUpper)
-- Functor: apply a pure function inside a structure, leaving the structure
-- alone. fmap over Maybe skips Nothing; fmap over a list is map.
upperMaybe :: Maybe String -> Maybe String
upperMaybe = fmap (map toUpper)
doubled :: [Int]
doubled = fmap (* 2) [1, 2, 3]
-- <$> is fmap spelled as an operator.
lengths :: Maybe String -> Maybe Int
lengths name = length <$> name
data User = User
{ userName :: String
, userAge :: Int
}
deriving (Show, Eq)
-- Applicative: the function itself is inside the structure too, so several
-- independent values can be combined. If any of them is Nothing, so is the
-- result, and no case analysis is written by hand.
makeUser :: Maybe String -> Maybe Int -> Maybe User
makeUser name age = User <$> name <*> age
-- Monad: each step depends on the result of the one before it.
half :: Int -> Maybe Int
half n
| even n = Just (n `div` 2)
| otherwise = Nothing
quarter :: Int -> Maybe Int
quarter n = half n >>= half
-- do notation is exactly the same chain with names for the intermediates.
eighth :: Int -> Maybe Int
eighth n = do
a <- half n
b <- half a
half b
-- The list monad means "every combination", which is the comprehension you
-- already know wearing different clothes.
pairs :: [(Int, Char)]
pairs = do
n <- [1, 2, 3]
c <- "ab"
pure (n, c)
-- traverse runs an action for every element and turns a list of results
-- inside out: [Maybe a] becomes Maybe [a], all or nothing.
allHalves :: [Int] -> Maybe [Int]
allHalves = traverse half
ghci> lookup "ada" [("ada", 36), ("grace", 45)]
Just 36
ghci> lookup "alan" [("ada", 36), ("grace", 45)]
Nothing
ghci> fmap (+ 1) (Just 36)
Just 37
ghci> fmap (+ 1) Nothing
Nothing
ghci> (+) <$> Just 1 <*> Just 2
Just 3
ghci> (+) <$> Just 1 <*> Nothing
Nothing
ghci> Just 36 >>= \age -> if age > 18 then Just "adult" else Nothing
Just "adult"
ghci> sequence [Just 1, Just 2, Just 3]
Just [1,2,3]
ghci> sequence [Just 1, Nothing, Just 3]
Nothing
The same operators, four structures
What makes this worth learning is that one set of operators works across types that have nothing else in common.
- Maybe: the chain stops at the first
Nothing. - Either e: the chain stops at the first
Left, carrying the reason. - Lists: every combination, which is what a comprehension does.
- IO: run this effect, then use its result to decide the next one.
Write a function with a Monad m => constraint and it works for all of them at once. That is the payoff, and it is why the vocabulary is worth the initial confusion.
IO and do notation
Effects are values that describe what to do. Nothing happens until main runs them, and the type system keeps track of which functions are involved.
IO String is not a String. It is a description of an effect that will produce a String when it is run. Building one runs nothing at all. Only main is ever run, and everything it does is stitched together from smaller descriptions.
This is why the type is worth having. A function with IO in its signature might read the disk, print, or launch a thread. A function without it cannot, no matter who wrote it.
module Main (main) where
import Data.Char (toUpper)
import System.IO (hFlush, stdout)
-- IO String is a description of an effect that yields a String when run.
-- Building one runs nothing; only main is ever run.
prompt :: String -> IO String
prompt question = do
putStr question
hFlush stdout -- putStr does not flush, so ask for it before reading
getLine
main :: IO ()
main = do
-- <- runs an action and names its result.
name <- prompt "What is your name? "
-- let names a pure value. No arrow, because nothing is being run.
let greeting = "Hello, " ++ map toUpper name
putStrLn greeting
writeFile "greeting.txt" (greeting ++ "\n")
saved <- readFile "greeting.txt"
putStr ("greeting.txt now holds: " ++ saved)
-- mapM_ is the for loop: an action for each element, results discarded.
mapM_ print [1 .. 3 :: Int]
The arrow and the equals sign
| Written as | Means | Use when |
|---|---|---|
| name <- getLine | run the action, name the result | the right hand side has IO in its type |
| let shout = map toUpper name | name a pure value | the right hand side is an ordinary expression |
| putStrLn greeting | run an action, ignore its () | you only want the effect |
Mixing those two up is the most common early mistake, and the error message is usually about a type not matching IO. If the thing on the right does effects, use the arrow. If it does not, use let.
The shape of a real program
The convention that makes Haskell programs pleasant is a thin IO shell around a large pure core. Read the input, parse it, hand it to functions that have no IO in their types, then write the result out.
Every chapter in the next section follows that shape. The interesting decisions live in functions you can test in GHCi without a file system, a network or a mock, and the IO part stays small enough to check by reading it.
Build: guess the number
A loop with no loop, input that cannot crash on bad text, and a three way comparison that has no default case.
The program picks a number, reads guesses, and says higher or lower until you get it. It is the smallest interesting program that needs randomness, input, a loop and a counter, which makes it a good first thing to build.
module Main (main) where
import System.IO (hFlush, stdout)
import System.Random (randomRIO)
import Text.Read (readMaybe)
main :: IO ()
main = do
secret <- randomRIO (1, 100 :: Int)
putStrLn "I picked a number between 1 and 100."
attempts <- play secret 1
putStrLn ("Solved it in " ++ show attempts ++ " guesses.")
-- There is no while loop. The loop is a function that calls itself, and the
-- guess counter is an argument rather than a variable being reassigned.
play :: Int -> Int -> IO Int
play secret attempt = do
putStr "> "
hFlush stdout
entered <- getLine
-- readMaybe parses without throwing: Nothing means it was not a number.
case readMaybe entered of
Nothing -> do
putStrLn "That is not a number."
play secret attempt
Just guess -> case compare guess secret of
LT -> do
putStrLn "Higher."
play secret (attempt + 1)
GT -> do
putStrLn "Lower."
play secret (attempt + 1)
EQ -> pure attempt
# Snippets that use a package outside GHC's own libraries need it installed
# first. Inside a cabal project, add it to build-depends instead.
cabal install --lib random
runghc guess.hs
randomRIO comes from the random package, which does not ship with GHC. Install it once with cabal, or add it to build-depends inside a project.
import random
def main() -> None:
secret = random.randint(1, 100)
print("I picked a number between 1 and 100.")
attempt = 1
while True:
entered = input("> ").strip()
try:
guess = int(entered)
except ValueError:
print("That is not a number.")
continue
if guess < secret:
print("Higher.")
elif guess > secret:
print("Lower.")
else:
print(f"Solved it in {attempt} guesses.")
return
attempt += 1
if __name__ == "__main__":
main()
import * as readline from "node:readline/promises";
import { stdin as input, stdout as output } from "node:process";
async function main() {
const rl = readline.createInterface({ input, output });
const secret = Math.floor(Math.random() * 100) + 1;
console.log("I picked a number between 1 and 100.");
let attempt = 1;
for (;;) {
const entered = (await rl.question("> ")).trim();
const guess = Number(entered);
if (!Number.isInteger(guess)) {
console.log("That is not a number.");
continue;
}
if (guess < secret) console.log("Higher.");
else if (guess > secret) console.log("Lower.");
else {
console.log(`Solved it in ${attempt} guesses.`);
break;
}
attempt += 1;
}
rl.close();
}
main();
Three things worth noticing
The loop is a function. play calls itself with attempt + 1. There is no counter being reassigned, so there is no chance of forgetting to increment it on one branch.
Bad input is a value. readMaybe returns Nothing rather than throwing, so the case for "that is not a number" sits next to the case for a real guess instead of in a catch block somewhere else.
The comparison has exactly three outcomes. compare gives LT, EQ or GT, and the match covers all three. There is no default branch, because there is nothing left to default to.
Try it yourself
Give the player five guesses and reveal the answer when they run out. Then keep a list of the guesses so far and print it at the end.
Hint: Both changes are new parameters on play. Nothing else has to move.
Show one solution Hide the solution
module Main (main) where
import System.IO (hFlush, stdout)
import System.Random (randomRIO)
import Text.Read (readMaybe)
main :: IO ()
main = do
secret <- randomRIO (1, 100 :: Int)
putStrLn "I picked a number between 1 and 100."
attempts <- play secret 1
putStrLn ("Solved it in " ++ show attempts ++ " guesses.")
-- There is no while loop. The loop is a function that calls itself, and the
-- guess counter is an argument rather than a variable being reassigned.
play :: Int -> Int -> IO Int
play secret attempt = do
putStr "> "
hFlush stdout
entered <- getLine
-- readMaybe parses without throwing: Nothing means it was not a number.
case readMaybe entered of
Nothing -> do
putStrLn "That is not a number."
play secret attempt
Just guess -> case compare guess secret of
LT -> do
putStrLn "Higher."
play secret (attempt + 1)
GT -> do
putStrLn "Lower."
play secret (attempt + 1)
EQ -> pure attempt
Build: an arithmetic drill
Keeping score without a mutable counter, and what record update actually does.
Same loop as the guessing game, with something extra to carry: a score with two numbers in it. In Python you would reach for a small class and edit its fields. Here the score is a value, and each answer produces a new one.
module Main (main) where
import System.IO (hFlush, stdout)
import System.Random (randomRIO)
import Text.Read (readMaybe)
-- The running score is a value, so it can be printed, tested and passed
-- around without any chance of another part of the program editing it.
data Score = Score
{ correct :: Int
, asked :: Int
}
main :: IO ()
main = do
putStrLn "Addition drill. Type quit to stop."
final <- drill (Score 0 0)
putStrLn (report final)
report :: Score -> String
report score =
show (correct score) ++ " correct out of " ++ show (asked score)
drill :: Score -> IO Score
drill score = do
a <- randomRIO (1, 10 :: Int)
b <- randomRIO (1, 10 :: Int)
putStr ("What is " ++ show a ++ " + " ++ show b ++ "? ")
hFlush stdout
entered <- getLine
if entered == "quit"
then pure score
else do
let answer = a + b
isRight = readMaybe entered == Just answer
putStrLn (if isRight then "Correct." else "No, it was " ++ show answer ++ ".")
drill (record isRight score)
-- A new Score is returned. The old one still exists, unchanged.
record :: Bool -> Score -> Score
record isRight score
| isRight = score { correct = correct score + 1, asked = asked score + 1 }
| otherwise = score { asked = asked score + 1 }
import random
from dataclasses import dataclass
@dataclass
class Score:
correct: int = 0
asked: int = 0
def main() -> None:
print("Addition drill. Type quit to stop.")
score = Score()
while True:
a, b = random.randint(1, 10), random.randint(1, 10)
entered = input(f"What is {a} + {b}? ").strip()
if entered == "quit":
break
answer = a + b
is_right = entered.lstrip("-").isdigit() and int(entered) == answer
print("Correct." if is_right else f"No, it was {answer}.")
# The score object is edited in place, so anything holding a
# reference to it sees the change.
score.asked += 1
score.correct += int(is_right)
print(f"{score.correct} correct out of {score.asked}")
if __name__ == "__main__":
main()
import * as readline from "node:readline/promises";
import { stdin as input, stdout as output } from "node:process";
async function main() {
const rl = readline.createInterface({ input, output });
console.log("Addition drill. Type quit to stop.");
const score = { correct: 0, asked: 0 };
for (;;) {
const a = Math.floor(Math.random() * 10) + 1;
const b = Math.floor(Math.random() * 10) + 1;
const entered = (await rl.question(`What is ${a} + ${b}? `)).trim();
if (entered === "quit") break;
const answer = a + b;
const isRight = Number(entered) === answer;
console.log(isRight ? "Correct." : `No, it was ${answer}.`);
score.asked += 1;
score.correct += isRight ? 1 : 0;
}
console.log(`${score.correct} correct out of ${score.asked}`);
rl.close();
}
main();
Both versions edit the score in place. Anything else holding a reference to it sees the change, which is convenient until it is not.
Record update is a copy
score { correct = correct score + 1 } builds a new Score with one field different. The original still exists and still has its old value, which is what makes it safe to hand a score to another function without wondering what it might do to it.
This sounds expensive and mostly is not. The fields that did not change are shared rather than copied, so a record update allocates one small object, not a deep clone.
Build: menus as a state machine
Screens become a data type, transitions become function calls, and an unreachable screen becomes a compile error.
Once a program has more than one screen, the usual result is a pile of booleans and a while loop nobody wants to touch. Giving the screens a type turns the same program into something you can read top to bottom.
module Main (main) where
import System.IO (hFlush, stdout)
import System.Random (randomRIO)
import Text.Read (readMaybe)
-- Two small types make the illegal states unrepresentable: there is no way
-- to be on a screen that does not exist, or to pick an operation that has
-- no implementation.
data Screen = Menu | Playing | Done
deriving (Show, Eq)
data Operation = Add | Multiply
deriving (Show, Eq)
data Settings = Settings
{ lowest :: Int
, highest :: Int
, operation :: Operation
}
deriving (Show)
defaults :: Settings
defaults = Settings { lowest = 1, highest = 10, operation = Add }
main :: IO ()
main = run Menu defaults
-- The whole state of the program is the pair of arguments. Every transition
-- is an ordinary function call, which makes the state machine easy to read
-- top to bottom and impossible to corrupt from somewhere else.
run :: Screen -> Settings -> IO ()
run Done _ = putStrLn "Bye."
run Menu settings = do
putStrLn ""
putStrLn (summarise settings)
putStr "1 play 2 switch operation 3 double the range 4 quit\n> "
hFlush stdout
choice <- getLine
case choice of
"1" -> run Playing settings
"2" -> run Menu settings { operation = other (operation settings) }
"3" -> run Menu settings { highest = highest settings * 2 }
"4" -> run Done settings
_ -> do
putStrLn "Pick 1, 2, 3 or 4."
run Menu settings
run Playing settings = do
a <- randomRIO (lowest settings, highest settings)
b <- randomRIO (lowest settings, highest settings)
let (symbol, answer) = case operation settings of
Add -> ("+", a + b)
Multiply -> ("*", a * b)
putStr (show a ++ " " ++ symbol ++ " " ++ show b ++ " = ? (menu to go back) ")
hFlush stdout
entered <- getLine
if entered == "menu"
then run Menu settings
else do
putStrLn
( if readMaybe entered == Just answer
then "Correct."
else "It was " ++ show answer ++ "."
)
run Playing settings
summarise :: Settings -> String
summarise settings =
show (operation settings)
++ ", numbers from "
++ show (lowest settings)
++ " to "
++ show (highest settings)
other :: Operation -> Operation
other Add = Multiply
other Multiply = Add
import random
from dataclasses import dataclass, replace
from enum import Enum, auto
class Screen(Enum):
MENU = auto()
PLAYING = auto()
DONE = auto()
class Operation(Enum):
ADD = auto()
MULTIPLY = auto()
@dataclass(frozen=True)
class Settings:
lowest: int = 1
highest: int = 10
operation: Operation = Operation.ADD
def main() -> None:
screen, settings = Screen.MENU, Settings()
while screen is not Screen.DONE:
if screen is Screen.MENU:
screen, settings = menu(settings)
else:
screen, settings = playing(settings)
print("Bye.")
def menu(settings: Settings) -> tuple[Screen, Settings]:
print()
print(f"{settings.operation.name}, numbers from {settings.lowest} to {settings.highest}")
choice = input("1 play 2 switch operation 3 double the range 4 quit\n> ").strip()
if choice == "1":
return Screen.PLAYING, settings
if choice == "2":
other = Operation.MULTIPLY if settings.operation is Operation.ADD else Operation.ADD
return Screen.MENU, replace(settings, operation=other)
if choice == "3":
return Screen.MENU, replace(settings, highest=settings.highest * 2)
if choice == "4":
return Screen.DONE, settings
print("Pick 1, 2, 3 or 4.")
return Screen.MENU, settings
def playing(settings: Settings) -> tuple[Screen, Settings]:
a = random.randint(settings.lowest, settings.highest)
b = random.randint(settings.lowest, settings.highest)
symbol, answer = ("+", a + b) if settings.operation is Operation.ADD else ("*", a * b)
entered = input(f"{a} {symbol} {b} = ? (menu to go back) ").strip()
if entered == "menu":
return Screen.MENU, settings
print("Correct." if entered == str(answer) else f"It was {answer}.")
return Screen.PLAYING, settings
if __name__ == "__main__":
main()
import * as readline from "node:readline/promises";
import { stdin as input, stdout as output } from "node:process";
const Screen = Object.freeze({ Menu: "Menu", Playing: "Playing", Done: "Done" });
const Operation = Object.freeze({ Add: "Add", Multiply: "Multiply" });
async function main() {
const rl = readline.createInterface({ input, output });
let screen = Screen.Menu;
let settings = { lowest: 1, highest: 10, operation: Operation.Add };
while (screen !== Screen.Done) {
if (screen === Screen.Menu) {
console.log();
console.log(
`${settings.operation}, numbers from ${settings.lowest} to ${settings.highest}`,
);
const choice = (
await rl.question("1 play 2 switch operation 3 double the range 4 quit\n> ")
).trim();
if (choice === "1") screen = Screen.Playing;
else if (choice === "2")
settings = {
...settings,
operation:
settings.operation === Operation.Add ? Operation.Multiply : Operation.Add,
};
else if (choice === "3") settings = { ...settings, highest: settings.highest * 2 };
else if (choice === "4") screen = Screen.Done;
else console.log("Pick 1, 2, 3 or 4.");
continue;
}
const span = settings.highest - settings.lowest + 1;
const a = settings.lowest + Math.floor(Math.random() * span);
const b = settings.lowest + Math.floor(Math.random() * span);
const symbol = settings.operation === Operation.Add ? "+" : "*";
const answer = settings.operation === Operation.Add ? a + b : a * b;
const entered = (
await rl.question(`${a} ${symbol} ${b} = ? (menu to go back) `)
).trim();
if (entered === "menu") {
screen = Screen.Menu;
continue;
}
console.log(Number(entered) === answer ? "Correct." : `It was ${answer}.`);
}
console.log("Bye.");
rl.close();
}
main();
Why this version is easier to change
run has one equation per screen. Adding a settings screen means adding a constructor, and the compiler immediately points at every place that now needs a branch. Nothing is missed, because nothing can be.
The settings are a record, so a menu choice returns a modified copy and passes it to the next call. There is no shared configuration object that some other part of the program might have edited between screens.
Build: flashcards
Running an action for each item in a list, collecting the answers, and shuffling a deck without a mutable array.
A deck of cards, asked in a random order, with a score at the end. The new ingredient is running an effect once per element of a list, which is the everyday replacement for a for loop over IO.
module Main (main) where
import Data.Char (toLower)
import Data.List (sortOn)
import System.IO (hFlush, stdout)
import System.Random (randomRIO)
data Card = Card
{ question :: String
, answer :: String
}
deck :: [Card]
deck =
[ Card "Which operator composes two functions?" "."
, Card "Which type says a value may be missing?" "Maybe"
, Card "Which fold is strict in its accumulator?" "foldl'"
, Card "Which tool installs GHC, cabal and HLS?" "ghcup"
]
main :: IO ()
main = do
shuffled <- shuffle deck
-- mapM runs an action for each card and collects the answers.
-- mapM_ is the same when the results are not wanted.
results <- mapM ask shuffled
let right = length (filter id results)
putStrLn (show right ++ " of " ++ show (length results) ++ " correct.")
ask :: Card -> IO Bool
ask card = do
putStr (question card ++ " ")
hFlush stdout
given <- getLine
let isRight = normalise given == normalise (answer card)
putStrLn (if isRight then "Correct." else "The answer was " ++ answer card)
pure isRight
-- Trim, collapse the inner spaces and ignore case, all with library
-- functions and no regular expression in sight.
normalise :: String -> String
normalise = unwords . words . map toLower
-- Give every card a random key, sort by the key, then throw the keys away.
shuffle :: [a] -> IO [a]
shuffle xs = do
keys <- mapM (const (randomRIO (0, 1 :: Double))) xs
pure (map snd (sortOn fst (zip keys xs)))
The mapM family
| Function | What it does |
|---|---|
| mapM f xs | run f on each element, collect the results |
| mapM_ f xs | the same, throw the results away |
| forM xs f | mapM with the arguments swapped, reads better with a lambda |
| forM_ xs f | the for loop you write most often |
| replicateM n a | run the same action n times, collect n results |
| sequence xs | turn a list of actions into an action returning a list |
| filterM p xs | filter, when the test itself needs IO |
The trailing underscore always means "discard the results". Use it when you only want the effects, because collecting a list of () values you never look at is a small waste and a misleading signal to the reader.
Shuffling without mutation
The usual shuffle swaps elements of an array in place, which needs mutation. The version here gives every card a random key, sorts by the key and throws the keys away. It is a few lines, it is pure apart from generating the numbers, and it is fast enough for anything that fits on a screen.
For a large deck, or for cryptographic quality, use System.Random.Shuffle or a mutable vector. For forty flashcards this is the right amount of machinery.
Try it yourself
Ask only the cards that were answered wrongly, in a second pass, and keep going until the deck is clear.
Hint: zip the results with the deck, keep the failures, and recurse when that list is not empty.
Show one solution Hide the solution
module Main (main) where
import Data.Char (toLower)
import Data.List (sortOn)
import System.IO (hFlush, stdout)
import System.Random (randomRIO)
data Card = Card
{ question :: String
, answer :: String
}
deck :: [Card]
deck =
[ Card "Which operator composes two functions?" "."
, Card "Which type says a value may be missing?" "Maybe"
, Card "Which fold is strict in its accumulator?" "foldl'"
, Card "Which tool installs GHC, cabal and HLS?" "ghcup"
]
main :: IO ()
main = do
shuffled <- shuffle deck
-- mapM runs an action for each card and collects the answers.
-- mapM_ is the same when the results are not wanted.
results <- mapM ask shuffled
let right = length (filter id results)
putStrLn (show right ++ " of " ++ show (length results) ++ " correct.")
ask :: Card -> IO Bool
ask card = do
putStr (question card ++ " ")
hFlush stdout
given <- getLine
let isRight = normalise given == normalise (answer card)
putStrLn (if isRight then "Correct." else "The answer was " ++ answer card)
pure isRight
-- Trim, collapse the inner spaces and ignore case, all with library
-- functions and no regular expression in sight.
normalise :: String -> String
normalise = unwords . words . map toLower
-- Give every card a random key, sort by the key, then throw the keys away.
shuffle :: [a] -> IO [a]
shuffle xs = do
keys <- mapM (const (randomRIO (0, 1 :: Double))) xs
pure (map snd (sortOn fst (zip keys xs)))
Build: counting words in a file
Reading a file, counting with a Map, sorting by frequency, and keeping all the interesting parts pure.
The classic exercise, and a good one, because it shows the shape a Haskell program usually takes: a small amount of IO at the edges and pure functions in the middle doing all the work.
module Main (main) where
import Data.Char (isAlpha, toLower)
import Data.List (sortOn)
import qualified Data.Map.Strict as Map
import Data.Ord (Down (..))
import System.Environment (getArgs)
main :: IO ()
main = do
args <- getArgs
case args of
[path] -> do
contents <- readFile path
mapM_ report (take 10 (ranked (tally contents)))
_ -> putStrLn "usage: wordcount FILE"
report :: (String, Int) -> IO ()
report (word, count) = putStrLn (pad word ++ show count)
where
pad w = w ++ replicate (max 1 (16 - length w)) ' '
-- Everything below is pure: a String goes in and a Map comes out. It can be
-- tested in GHCi without a file, and reused from a web handler unchanged.
tokens :: String -> [String]
tokens = words . map keepLetters
where
keepLetters c
| isAlpha c = toLower c
| otherwise = ' '
-- insertWith adds one when the word is new, and adds to the count when it
-- is not. Data.Map is a balanced tree, not a hash table, so it stays sorted.
tally :: String -> Map.Map String Int
tally = foldr bump Map.empty . tokens
where
bump word = Map.insertWith (+) word 1
-- Down flips the ordering, so this sorts from most common to least.
ranked :: Map.Map String Int -> [(String, Int)]
ranked = sortOn (Down . snd) . Map.toList
import sys
from collections import Counter
def tokens(text: str) -> list[str]:
return "".join(c.lower() if c.isalpha() else " " for c in text).split()
def tally(text: str) -> Counter[str]:
return Counter(tokens(text))
def main() -> None:
if len(sys.argv) != 2:
print("usage: wordcount FILE")
return
with open(sys.argv[1], encoding="utf-8") as handle:
contents = handle.read()
for word, count in tally(contents).most_common(10):
print(f"{word:<16}{count}")
if __name__ == "__main__":
main()
import { readFile } from "node:fs/promises";
import { argv } from "node:process";
export const tokens = (text) =>
[...text.toLowerCase()]
.map((c) => (/\p{L}/u.test(c) ? c : " "))
.join("")
.split(/\s+/)
.filter(Boolean);
export function tally(text) {
const counts = new Map();
for (const word of tokens(text)) {
counts.set(word, (counts.get(word) ?? 0) + 1);
}
return counts;
}
async function main() {
const path = argv[2];
if (!path) {
console.log("usage: wordcount FILE");
return;
}
const contents = await readFile(path, "utf8");
const ranked = [...tally(contents)].sort((a, b) => b[1] - a[1]).slice(0, 10);
for (const [word, count] of ranked) {
console.log(word.padEnd(16) + count);
}
}
main();
Python's Counter does more of the work. The Haskell version spells the fold out, which is a fair trade for being able to test tally without a file.
Data.Map is a tree, not a hash table
Data.Map is a balanced binary tree, so keys need Ord rather than a hash, lookups are logarithmic rather than constant, and the contents come out sorted for free. Import it qualified, always, because it deliberately reuses names like lookup and filter from the Prelude.
insertWith is the function to learn first. It inserts when the key is new and combines with the existing value when it is not, which is the whole of counting in one call. Use Data.Map.Strict unless you have a reason not to, so the counts do not pile up as unevaluated additions.
Why the pure part matters
tokens, tally and ranked have no IO in their types. That means you can load the file into GHCi and try them on a string literal, and it means the property test in the testing chapter needs no fixtures. The only part that has to be run as a program is the six lines that read a path from the command line.
Try it yourself
Add a stop word list so "the", "and" and friends are left out, and make the number of results a second command line argument.
Hint: getArgs returns a list, so match on [path], [path, n] and anything else.
Show one solution Hide the solution
module Main (main) where
import Data.Char (isAlpha, toLower)
import Data.List (sortOn)
import qualified Data.Map.Strict as Map
import Data.Ord (Down (..))
import System.Environment (getArgs)
main :: IO ()
main = do
args <- getArgs
case args of
[path] -> do
contents <- readFile path
mapM_ report (take 10 (ranked (tally contents)))
_ -> putStrLn "usage: wordcount FILE"
report :: (String, Int) -> IO ()
report (word, count) = putStrLn (pad word ++ show count)
where
pad w = w ++ replicate (max 1 (16 - length w)) ' '
-- Everything below is pure: a String goes in and a Map comes out. It can be
-- tested in GHCi without a file, and reused from a web handler unchanged.
tokens :: String -> [String]
tokens = words . map keepLetters
where
keepLetters c
| isAlpha c = toLower c
| otherwise = ' '
-- insertWith adds one when the word is new, and adds to the count when it
-- is not. Data.Map is a balanced tree, not a hash table, so it stays sorted.
tally :: String -> Map.Map String Int
tally = foldr bump Map.empty . tokens
where
bump word = Map.insertWith (+) word 1
-- Down flips the ordering, so this sorts from most common to least.
ranked :: Map.Map String Int -> [(String, Int)]
ranked = sortOn (Down . snd) . Map.toList
Build: tidy up a downloads folder
A real script that moves files into folders by type, with the decision kept in a function that never touches the disk.
This is the sort of thing most people write in Python at some point. It is worth writing in Haskell once, because it makes the split between the part that decides and the part that acts unusually visible.
module Main (main) where
import Control.Monad (filterM, forM_)
import Data.Char (toLower)
import System.Directory
( createDirectoryIfMissing
, doesFileExist
, listDirectory
, renameFile
)
import System.Environment (getArgs)
import System.FilePath (takeExtension, (</>))
main :: IO ()
main = do
args <- getArgs
let folder = case args of
[given] -> given
_ -> "."
entries <- listDirectory folder
-- filterM is filter for tests that need IO, such as asking the file
-- system whether something is a file or a directory.
files <- filterM (doesFileExist . (folder </>)) entries
forM_ files (move folder)
putStrLn ("Sorted " ++ show (length files) ++ " files.")
move :: FilePath -> FilePath -> IO ()
move folder file = do
let target = folder </> categoryOf file
createDirectoryIfMissing True target
renameFile (folder </> file) (target </> file)
putStrLn (file ++ " -> " ++ categoryOf file)
-- The interesting decision is a pure function. It needs no file system, so
-- it can be checked in GHCi or covered by a property test in seconds.
categoryOf :: FilePath -> String
categoryOf file
| ext `elem` ["pdf", "doc", "docx", "txt", "md"] = "documents"
| ext `elem` ["jpg", "jpeg", "png", "gif", "webp"] = "images"
| ext `elem` ["mp4", "mov", "mkv", "mp3", "wav"] = "media"
| ext `elem` ["zip", "tar", "gz", "7z"] = "archives"
| otherwise = "other"
where
ext = map toLower (drop 1 (takeExtension file))
import sys
from pathlib import Path
CATEGORIES = {
"documents": {".pdf", ".doc", ".docx", ".txt", ".md"},
"images": {".jpg", ".jpeg", ".png", ".gif", ".webp"},
"media": {".mp4", ".mov", ".mkv", ".mp3", ".wav"},
"archives": {".zip", ".tar", ".gz", ".7z"},
}
def category_of(path: Path) -> str:
suffix = path.suffix.lower()
for name, suffixes in CATEGORIES.items():
if suffix in suffixes:
return name
return "other"
def main() -> None:
folder = Path(sys.argv[1] if len(sys.argv) > 1 else ".")
files = [entry for entry in folder.iterdir() if entry.is_file()]
for entry in files:
target = folder / category_of(entry)
target.mkdir(parents=True, exist_ok=True)
entry.rename(target / entry.name)
print(f"{entry.name} -> {category_of(entry)}")
print(f"Sorted {len(files)} files.")
if __name__ == "__main__":
main()
import { mkdir, readdir, rename, stat } from "node:fs/promises";
import { extname, join } from "node:path";
import { argv } from "node:process";
const CATEGORIES = {
documents: [".pdf", ".doc", ".docx", ".txt", ".md"],
images: [".jpg", ".jpeg", ".png", ".gif", ".webp"],
media: [".mp4", ".mov", ".mkv", ".mp3", ".wav"],
archives: [".zip", ".tar", ".gz", ".7z"],
};
export function categoryOf(name) {
const suffix = extname(name).toLowerCase();
for (const [category, suffixes] of Object.entries(CATEGORIES)) {
if (suffixes.includes(suffix)) return category;
}
return "other";
}
async function main() {
const folder = argv[2] ?? ".";
const entries = await readdir(folder);
let moved = 0;
for (const entry of entries) {
const info = await stat(join(folder, entry));
if (!info.isFile()) continue;
const target = join(folder, categoryOf(entry));
await mkdir(target, { recursive: true });
await rename(join(folder, entry), join(target, entry));
console.log(`${entry} -> ${categoryOf(entry)}`);
moved += 1;
}
console.log(`Sorted ${moved} files.`);
}
main();
filterM, and why it exists
filter takes a test that returns Bool. Asking the file system whether something is a file returns IO Bool, which does not fit. filterM is the same function for tests that need effects, and it appears in the type as (a -> m Bool) -> [a] -> m [a].
Most list functions have a monadic twin like this: mapM for map, foldM for foldl, zipWithM for zipWith. When a pure function almost fits, look for the version with M on the end.
The part worth stealing
categoryOf is a pure function from a filename to a folder name. It needs no disk, so it can be tested exhaustively in a second, and the rules can be changed without going near the code that moves anything. When a script goes wrong it is almost always the decision that was wrong rather than the moving, and this shape puts the decision where you can look at it.
Laziness, strictness and space
Nothing is evaluated until it is needed. That gives you infinite lists and free short circuiting, and one performance trap worth understanding early.
Haskell is lazy by default, which is unusual enough to be worth stating plainly: writing an expression does not compute it. It builds a small promise, called a thunk, and the value is produced only when something actually needs it.
module Laziness where
import Data.List (foldl')
-- Nothing is evaluated until something needs it, so an endless list is an
-- ordinary value rather than a hang.
naturals :: [Integer]
naturals = [1 ..]
firstFive :: [Integer]
firstFive = take 5 naturals
-- The second element of this pair is never looked at, so it never explodes.
survives :: Int
survives = fst (42, undefined)
-- Short circuiting is not a special rule for && and ||. It falls out of
-- laziness, and your own functions get it too.
anyNegative :: [Int] -> Bool
anyNegative = any (< 0)
-- The classic space leak. foldl builds a tower of pending additions and
-- only collapses it at the end, which can exhaust memory on a long list.
slowSum :: [Int] -> Int
slowSum = foldl (+) 0
-- foldl' forces the running total at every step and runs in constant space.
fastSum :: [Int] -> Int
fastSum = foldl' (+) 0
-- seq forces its first argument when the result is forced. ($!) is the same
-- idea for function application.
strictLength :: [a] -> Int
strictLength = go 0
where
go acc [] = acc
go acc (_ : xs) = acc `seq` go (acc + 1) xs
gain
Infinite structures
[1 ..] and self referencing definitions like fibs are ordinary values. You write the whole sequence and take the part you want.
gain
Short circuiting everywhere
&& and || stop early because of laziness, not because of a special rule, so your own functions get the same behaviour without asking.
gain
Separation of producer and consumer
take 10 (filter isPrime [1 ..]) reads as two independent ideas and runs as one loop that stops at the right moment. No generator syntax needed.
cost
Space leaks
Thunks take memory. A lazy accumulator over a million element list builds a million pending additions before collapsing them, and that is where surprise memory use comes from.
The one rule that avoids most trouble
Use foldl' from Data.List, not foldl, whenever you are accumulating a number over a long list. It forces the running total at each step, so the program uses constant space instead of building a tower.
Similarly, prefer Data.Map.Strict over Data.Map.Lazy when the values are counts or sums. That one import decision removes the most common source of unexplained memory growth in real programs.
Threads, MVar and STM
Immutability removes most of the reasons data races happen. What is left is handled by a transaction system rather than by locks you have to remember.
Most concurrency bugs come from two threads touching the same mutable value. In Haskell almost nothing is mutable, so most of the problem is gone before you start. What remains is deliberate shared state, and the runtime gives you two good tools for it.
GHC threads are green threads managed by the runtime, not operating system threads. They cost a few hundred bytes, so starting ten thousand of them is a normal design rather than a stunt, and they are scheduled across real cores when you compile with -threaded.
MVar: a box with one slot
module Main (main) where
import Control.Concurrent (forkIO, newEmptyMVar, putMVar, takeMVar)
main :: IO ()
main = do
-- An MVar is a box that is either empty or full, and it is safe to share.
box <- newEmptyMVar
-- forkIO starts a green thread. They cost a few hundred bytes each, so
-- tens of thousands of them is a normal design rather than a stunt.
_ <- forkIO $ do
putStrLn "worker: adding up a million numbers"
putMVar box (sum [1 .. 1000000 :: Integer])
-- takeMVar blocks until the box is full, then empties it.
total <- takeMVar box
putStrLn ("main: the worker said " ++ show total)
An MVar is either empty or full. takeMVar blocks until it is full and empties it; putMVar blocks until it is empty and fills it. That is enough to build a mailbox, a mutex or a one shot result, and it is the right tool when one thread hands a value to another.
STM: transactions instead of locks
module Main (main) where
import Control.Concurrent (forkIO)
import Control.Concurrent.STM
( STM
, TVar
, atomically
, newTVarIO
, readTVar
, retry
, writeTVar
)
import Control.Monad (forM_, replicateM_)
main :: IO ()
main = do
counter <- newTVarIO (0 :: Int)
forM_ [1 .. 100 :: Int] $ \_ ->
forkIO (replicateM_ 100 (atomically (increment counter)))
-- retry parks this thread until one of the variables it read changes.
-- No polling loop, no sleep, no condition variable to get wrong.
final <- atomically $ do
value <- readTVar counter
if value < 10000
then retry
else pure value
putStrLn ("counted to " ++ show final)
-- A transaction. If two threads touch the same TVar at the same time, the
-- runtime throws one attempt away and runs it again, so a half finished
-- update is never visible and there is no lock to forget.
increment :: TVar Int -> STM ()
increment counter = do
value <- readTVar counter
writeTVar counter (value + 1)
import threading
counter = 0
lock = threading.Lock()
def bump(times: int) -> None:
global counter
for _ in range(times):
# Without the lock this loses updates, because counter += 1 is a
# read, an add and a write with room for another thread in between.
with lock:
counter += 1
def main() -> None:
workers = [threading.Thread(target=bump, args=(100,)) for _ in range(100)]
for worker in workers:
worker.start()
for worker in workers:
worker.join()
# The lock is correct here and easy to get wrong elsewhere: nothing in
# the language connects `counter` to the lock that is supposed to guard
# it. Threads also share one interpreter lock, so CPU bound work needs
# multiprocessing instead.
print(f"counted to {counter}")
if __name__ == "__main__":
main()
import { Worker, isMainThread, parentPort, workerData } from "node:worker_threads";
// One JavaScript thread runs at a time in a given context. Sharing a plain
// counter between workers is impossible, so state is copied through
// messages, or squeezed into a SharedArrayBuffer with manual Atomics.
if (isMainThread) {
const workers = Array.from({ length: 8 }, (_, i) =>
new Promise((resolve, reject) => {
const worker = new Worker(new URL(import.meta.url), { workerData: i + 1 });
worker.on("message", resolve);
worker.on("error", reject);
}),
);
const results = await Promise.all(workers);
console.log(`counted to ${results.reduce((a, b) => a + b, 0)}`);
} else {
let count = 0;
for (let i = 0; i < 100; i++) count += workerData > 0 ? 1 : 0;
parentPort.postMessage(count);
}
Python needs an explicit lock, and nothing in the language ties the lock to the data it protects. Node cannot share the counter at all, so the work is split and the results are added up at the end.
Everything inside atomically either happens completely or does not happen at all. If two threads collide, the runtime discards one attempt and runs it again. There is no lock to acquire in the wrong order, and no half finished update another thread can observe.
retry is the part with no real equivalent elsewhere. It says "not yet", and the runtime parks the thread until one of the variables the transaction read has changed. No polling, no sleep, no condition variable to get subtly wrong.
| Reach for | When |
|---|---|
MVar | handing one value from one thread to another, or a simple mutex |
TVar with atomically | shared state touched by several threads, or any update spanning two variables |
Chan or TQueue | a producer and consumer pipeline |
async package | running a handful of IO actions at once and waiting for the results |
par and pseq | pure computations you want spread across cores, with no shared state at all |
Testing and properties
Examples with hspec, and properties with QuickCheck, which invents the inputs for you and shrinks any failure to the smallest one that still breaks.
Pure functions are the easiest thing in the world to test. No mocks, no fixtures, no setup and teardown: call it, compare the answer. That is one of the practical reasons to keep the interesting logic out of IO.
-- A library module: no main, just functions other modules can import.
-- The export list after the module name is the public surface.
module Sorting
( quickSort
, isSorted
) where
quickSort :: Ord a => [a] -> [a]
quickSort [] = []
quickSort (pivot : rest) = quickSort smaller ++ [pivot] ++ quickSort larger
where
smaller = [x | x <- rest, x <= pivot]
larger = [x | x <- rest, x > pivot]
isSorted :: Ord a => [a] -> Bool
isSorted xs = and (zipWith (<=) xs (drop 1 xs))
-- test/Spec.hs. Run it with: cabal test
module Main (main) where
import Sorting (isSorted, quickSort)
import Test.Hspec
import Test.QuickCheck
main :: IO ()
main = hspec $ do
describe "quickSort" $ do
-- Example based tests, the kind you already write.
it "sorts a short list" $
quickSort [3, 1, 2 :: Int] `shouldBe` [1, 2, 3]
it "leaves the empty list alone" $
quickSort ([] :: [Int]) `shouldBe` []
-- Property based tests. QuickCheck invents a hundred lists per run and
-- shrinks any failure to the smallest example that still breaks.
it "always produces a sorted list" $
property $ \(xs :: [Int]) -> isSorted (quickSort xs)
it "keeps every element" $
property $ \(xs :: [Int]) -> length (quickSort xs) == length xs
it "is idempotent" $
property $ \(xs :: [Int]) -> quickSort (quickSort xs) == quickSort xs
Properties find the cases you would not have written
An example test says sorting [3,1,2] gives [1,2,3]. A property says sorting any list gives a sorted list with the same elements. QuickCheck then generates a hundred lists per run, including the empty one, the singleton, lists with duplicates and lists of negative numbers.
When it finds a failure it shrinks it: it keeps making the input smaller while the test still fails, so the report is the minimal counterexample rather than the random monster that happened to trigger the bug. That single feature makes a failing property test a pleasure to debug.
-
Look for a relationship, not a value
Good properties are laws: reversing twice gives the original, encoding then decoding is the identity, the output length matches the input length, sorting twice is the same as sorting once.
-
Compare against something simpler
If you optimised a function, keep the slow obvious version around and assert the two agree on every input. This catches more than any set of examples you would think to write.
-
Keep examples for the cases with meaning
Properties cover the general shape. Specific examples document the edge cases people actually asked about, and read better in a review than a property does.
cabal-version: 3.0
name: wordcount
version: 0.1.0.0
synopsis: Count the words in a file
license: BSD-3-Clause
build-type: Simple
-- Settings shared by every target below.
common warnings
ghc-options: -Wall -Wcompat -Wincomplete-record-updates
library
import: warnings
exposed-modules: Tally
hs-source-dirs: src
build-depends: base ^>=4.19, containers
default-language: GHC2021
executable wordcount
import: warnings
main-is: Main.hs
hs-source-dirs: app
build-depends: base ^>=4.19, wordcount
default-language: GHC2021
test-suite spec
import: warnings
type: exitcode-stdio-1.0
main-is: Spec.hs
hs-source-dirs: test
build-depends: base ^>=4.19, wordcount, hspec, QuickCheck
default-language: GHC2021
A test-suite stanza with type exitcode-stdio-1.0 is all cabal test needs. The common warnings block keeps the same flags on every target.
The libraries you will actually use
JSON, text, command line parsing, HTTP and concurrency, with the package names and enough code to see the shape of each one.
GHC ships with a useful set of libraries: base, containers, text, bytestring, directory, filepath, process, stm, time. Everything else comes from Hackage, and cabal fetches it when you add a line to build-depends.
| Need | Package | Note |
|---|---|---|
| JSON | aeson | derive instances from Generic and move on |
| Efficient text | text | use it instead of String for anything large |
| Byte strings | bytestring | binary data and network payloads |
| Maps and sets | containers | ships with GHC |
| Hash maps | unordered-containers | faster when you do not need sorted keys |
| Command line | optparse-applicative | generates --help for you |
| HTTP client | req or http-client | req has the friendlier interface |
| Web server | servant or scotty | types first, or express style |
| Databases | persistent, postgresql-simple | an ORM, or plain SQL |
| Concurrency | async | run several actions at once, safely |
| Testing | hspec, QuickCheck | examples and properties |
| Logging | katip or fast-logger | structured, or fast |
JSON with aeson
Derive Generic, ask for the two instances, and both directions are written for you. eitherDecode explains a parse failure; decode just returns Nothing.
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE OverloadedStrings #-}
module Main (main) where
import Data.Aeson (FromJSON, ToJSON, eitherDecode, encode)
import qualified Data.ByteString.Lazy.Char8 as ByteString
import GHC.Generics (Generic)
-- Deriving Generic is enough for aeson to write both directions for you.
data User = User
{ userId :: Int
, userName :: String
, isActive :: Bool
}
deriving (Show, Generic)
instance FromJSON User
instance ToJSON User
main :: IO ()
main = do
let raw = "{\"userId\":1,\"userName\":\"Ada\",\"isActive\":true}"
-- eitherDecode explains what went wrong; decode returns a bare Maybe.
case eitherDecode raw :: Either String User of
Left problem -> putStrLn ("bad JSON: " ++ problem)
Right user -> do
print user
ByteString.putStrLn (encode user)
Text instead of String
String is a linked list of characters, which is fine for a prompt and wasteful for a document. Text is packed and is what libraries expect. Turn on OverloadedStrings so literals work as either.
{-# LANGUAGE OverloadedStrings #-}
module Main (main) where
import qualified Data.Text as Text
import qualified Data.Text.IO as TextIO
-- String is a linked list of characters, which is fine for a prompt and
-- wasteful for a document. Text is a packed array, and it is what libraries
-- expect. OverloadedStrings lets a literal be either one.
main :: IO ()
main = do
let headline = " Modern Haskell, one Text at a time " :: Text.Text
cleaned = Text.strip headline
shouted = Text.toUpper cleaned
TextIO.putStrLn shouted
print (Text.length cleaned)
print (Text.splitOn ", " cleaned)
print (Text.replace "Haskell" "GHC" cleaned)
TextIO.writeFile "headline.txt" (cleaned <> "\n")
saved <- TextIO.readFile "headline.txt"
TextIO.putStr saved
Command line arguments
Each option is a small value, and the applicative operators glue them into a parser for a whole record. The --help output is generated from the same description.
module Main (main) where
import Options.Applicative
data Options = Options
{ name :: String
, count :: Int
, loud :: Bool
}
-- Each option parser is a small value, and <$> and <*> glue them into a
-- parser for the whole record. --help is generated from the descriptions.
options :: Parser Options
options =
Options
<$> strOption
( long "name" <> short 'n' <> metavar "NAME" <> help "Who to greet" )
<*> option auto
( long "count" <> short 'c' <> value 1 <> showDefault
<> metavar "N" <> help "How many times" )
<*> switch
( long "loud" <> help "Use capitals" )
main :: IO ()
main = do
parsed <- execParser description
mapM_ putStrLn (replicate (count parsed) (greeting parsed))
where
description = info
(options <**> helper)
(fullDesc <> progDesc "Greet someone from the command line")
greeting :: Options -> String
greeting parsed
| loud parsed = "HELLO, " ++ name parsed ++ "!"
| otherwise = "Hello, " ++ name parsed ++ "."
HTTP requests
The URL is assembled from typed pieces, so a GET with a body or a stray slash is a compile error rather than a puzzling 400.
{-# LANGUAGE OverloadedStrings #-}
module Main (main) where
import Control.Monad.IO.Class (liftIO)
import qualified Data.ByteString.Char8 as ByteString
import Network.HTTP.Req
-- The URL is built from typed pieces, so a stray slash or a GET with a body
-- is a compile error rather than a confusing 400 at three in the morning.
main :: IO ()
main = runReq defaultHttpConfig $ do
let url = https "api.github.com" /: "repos" /: "haskell" /: "cabal"
response <-
req
GET
url
NoReqBody
bsResponse
(header "User-Agent" "haskell-dev-guide")
liftIO (ByteString.putStrLn (responseBody response))
liftIO (print (responseStatusCode response))
Running things at the same time
mapConcurrently runs every action at once and waits for all of them. If one throws, the others are cancelled rather than left running in the background.
module Main (main) where
import Control.Concurrent (threadDelay)
import Control.Concurrent.Async (mapConcurrently)
-- mapConcurrently runs every action at once and waits for all of them. If
-- one throws, the rest are cancelled instead of being left running.
main :: IO ()
main = do
results <- mapConcurrently slowDouble [1 .. 8]
print (results :: [Int])
slowDouble :: Int -> IO Int
slowDouble n = do
threadDelay 200000 -- 200 ms, and none of these queue behind each other
pure (n * 2)
Projects, tooling and where to go next
How a real project is laid out, the four tools worth having on day one, and the reading that follows this guide.
A single file and runghc is the right setup while you are learning. Once a program has more than one module or needs a package from Hackage, move it into a cabal project, which takes about a minute.
# Start a project. The prompts pick a name, a licence and a layout.
mkdir wordcount && cd wordcount
cabal init --interactive
# Build, run and test.
cabal build
cabal run wordcount -- notes.txt
cabal test
# Open a prompt with your own modules and dependencies already loaded.
cabal repl
# Add a dependency by editing build-depends in the .cabal file, then:
cabal build
# Format and lint. Both integrate with the language server.
fourmolu --mode inplace app src
hlint src
The layout everyone uses
Library code goes in src/, the thin executable wrapper in app/, and tests in test/. Keeping the interesting code in the library rather than the executable is what lets the test suite import it, so it is worth doing even for small programs.
wordcount.cabaldeclares the targets and their dependencies.src/Tally.hsholds the pure logic, with a module name matching the file name.app/Main.hsreads arguments and calls into the library.test/Spec.hsis the test runner.cabal.projectappears once you have more than one package in the repository.
cabal-version: 3.0
name: wordcount
version: 0.1.0.0
synopsis: Count the words in a file
license: BSD-3-Clause
build-type: Simple
-- Settings shared by every target below.
common warnings
ghc-options: -Wall -Wcompat -Wincomplete-record-updates
library
import: warnings
exposed-modules: Tally
hs-source-dirs: src
build-depends: base ^>=4.19, containers
default-language: GHC2021
executable wordcount
import: warnings
main-is: Main.hs
hs-source-dirs: app
build-depends: base ^>=4.19, wordcount
default-language: GHC2021
test-suite spec
import: warnings
type: exitcode-stdio-1.0
main-is: Spec.hs
hs-source-dirs: test
build-depends: base ^>=4.19, wordcount, hspec, QuickCheck
default-language: GHC2021
Four tools worth having on day one
ghcup
Toolchain manager
Installs and switches between versions of GHC, cabal, stack and the language server. ghcup tui is a menu; nothing has to be uninstalled to try a newer compiler.
hls
Haskell Language Server
Types on hover, errors as you type, jump to definition, and code actions that fill in a missing type signature or add a missing case for you. It changes the learning experience more than any other single thing.
hoogle
Search by type
Look up a function by name, or by the shape of the thing you need. The local version, hoogle generate, indexes your own dependencies too.
fourmolu
Formatter and linter
fourmolu or ormolu settles formatting arguments once. hlint suggests simplifications, and it is a surprisingly good teacher in the first month.
Habits that make Haskell pleasant
-
Write the type signature first
Deciding what goes in and what comes out is most of the design. Once the signature is right the body is often the only thing that typechecks, and the compiler will tell you when it is not.
-
Keep IO at the edges
Read, parse, hand the data to pure functions, write the result. Every program in this guide is built that way, and it is what makes the interesting parts testable in GHCi.
-
Turn warnings on and leave them on
-Wallcatches missing cases, unused bindings and shadowed names. Most teams add-Werrorin CI so they never accumulate. -
Use GHCi constantly
:reloadafter every small change, and:typeon anything you are unsure about. A fast feedback loop matters more here than in most languages, because the compiler has so much to say. -
Let the error messages train you
They are verbose and they are precise. Read the expected type and the actual type, and ignore everything else until those two make sense. After a few weeks they stop being noise.
Where to go after this
| Resource | Good for |
|---|---|
| Learn You a Haskell (community edition) | a gentle second pass over the basics, kept up to date |
| Haskell Programming from First Principles | the thorough route, with exercises that actually teach |
| Parallel and Concurrent Programming in Haskell | the definitive treatment of threads, STM and parallelism |
| Hoogle | finding a function when you only know its type |
| Exercism, Haskell track | short problems with feedback from real reviewers |
| Haskell Discourse | asking questions without being told to read a category theory paper |