Foundations Key idea: Enough practical syntax to read every chapter that follows fluently

Base Camp: A Whirlwind Tour of the Haskell Landscape

Type signatures, pattern matching, guards, currying, sections, lambdas, recursion — the practical vocabulary every later chapter assumes you already have. If any of this is new, start here. If none of it is, skip ahead with a clear conscience.

Common Pitfall

This chapter is a detour, not a peak. Every chapter from here on assumes you can read an ordinary Haskell function definition fluently — pattern matching, guards, sections, currying, the works. If you already can, skip straight to Chapter 2 with a clear conscience; nothing below is a prerequisite you’ll be quizzed on later. If Haskell syntax is new to you, stay a while — this is the base camp the rest of the climb is launched from.

Getting Haskell onto your machine

Before any of the rest of this makes sense hands-on, you need a working compiler. The current standard way to get one is GHCup, which installs GHC (the Glasgow Haskell Compiler), the build tools Cabal and Stack, and the language server, all in one step, on Linux, macOS, and Windows alike. Once it’s installed, two commands matter most:

ghci             # an interactive prompt — type expressions, get answers
runghc file.hs   # run a .hs file directly, no separate build step

ghci is where you’ll live for most of this book. Load a file with :l filename.hs, ask for a value’s type with :t someExpression, and reload after editing with :r. Everything in this chapter can be typed straight into it.

The layout rule: whitespace that means something

Haskell doesn’t use { } or begin/end to mark blocks — indentation itself is the syntax. Definitions that belong together must line up:

-- these two equations belong to the same definition of greet
greet name
  | name == "" = "Hello, stranger."
  | otherwise  = "Hello, " ++ name ++ "!"
Common Pitfall

The single most common first error for newcomers is misaligned where/let bindings — Haskell is genuinely strict about this, and a one-space indentation mismatch produces a parse error that can look bewildering (“parse error on input…”) if you don’t yet know to look for a whitespace slip. If GHC ever complains about a line that looks completely fine, check the column it started on against its neighbours first.

Values, bindings, and types at a glance

A binding gives a name to a value, and (usually) a signature states its type above it:

age :: Int
age = 34

pi' :: Double
pi' = 3.14159

initial :: Char
initial = 'R'

greeting :: String
greeting = "hello"          -- String is really [Char], a list of Char

point :: (Int, Int)
point = (3, 4)               -- a tuple: fixed size, mixed types allowed

primes5 :: [Int]
primes5 = [2, 3, 5, 7, 11]    -- a list: any size, one type throughout

GHC almost never needs the :: line — it can infer every type above from the right-hand side alone. Type signatures on top-level definitions are written anyway, everywhere, because they’re the one piece of documentation the compiler actually checks for truth. Ask GHCi directly if you’re ever unsure:

ghci> :t True
True :: Bool
ghci> :t (3, "hi")
(3, "hi") :: (Num a, [Char])

Defining your own types: data, constructors, and newtype

Every type you’ve seen so far — Int, Bool, [a] — came from the standard library. The data keyword lets you define new ones, and it’s worth being comfortable with it before the pattern-matching examples in the next section, since almost every pattern you’ll write is taking one of these apart.

data Shape = Circle Double | Rectangle Double Double
  deriving (Show, Eq)
Anatomy of a data declaration: Shape is either a Circle holding one field or a Rectangle holding two fields

Figure: Shape is the type name. Circle and Rectangle are constructors — functions that build a Shape value, each holding a different number of fields. A value of type Shape is either one or the other, never both: this is why data declarations like this are called sum types.

Constructors are genuinely just functions — Circle :: Double -> Shape and Rectangle :: Double -> Double -> Shape — so building a value looks exactly like an ordinary function call:

myCircle :: Shape
myCircle = Circle 5.0

myRect :: Shape
myRect = Rectangle 3.0 4.0

And pattern matching, which you’ll see everywhere starting in the next section, is precisely how you get the fields back out:

area :: Shape -> Double
area (Circle r)      = pi * r * r
area (Rectangle w h) = w * h
Cool Fact

deriving (Show, Eq) asks GHC to automatically generate the boilerplate for converting a value to a printable String (Show) and comparing two values for equality (Eq), rather than you writing that logic by hand. It’s one of the most-used pieces of convenience syntax in everyday Haskell — nearly every data declaration ends with a deriving clause.

Product types: fields held together, not instead of

Rectangle Double Double holds two fields at once — a product type. Combine the two ideas and a data declaration is, in general, a sum of products: a choice of constructors, each bundling together whatever fields it needs.

data Person = Person String Int    -- one constructor, two fields: a product
  deriving Show

data LoginResult = Success Person | Failure String   -- a sum of two cases
  deriving Show

LoginResult reads exactly like the sentence it represents: a login either succeeds, carrying the logged-in Person, or fails, carrying a String explaining why — and the type system won’t let you forget to handle either case.

newtype: a zero-cost wrapper, not a new shape

newtype looks like data restricted to exactly one constructor with exactly one field — because that’s exactly what it is:

newtype UserId = UserId Int
  deriving (Show, Eq)

newtype Email = Email String
  deriving (Show, Eq)
Common Pitfall

It’s tempting to assume newtype is just data with extra restrictions for no real benefit. The restriction is the entire point: because a newtype can only ever have one constructor wrapping one value, GHC can guarantee it adds zero runtime cost — UserId and Int are represented identically in memory, and the wrapping/unwrapping vanishes entirely during compilation. What you do get, for free, is the type checker refusing to let you pass a raw Int where a UserId was expected, even though they’re indistinguishable once compiled. It’s a compile-time-only safety net, not a data structure.

sendWelcomeEmail :: Email -> IO ()
sendWelcomeEmail (Email addr) = putStrLn ("Welcome email sent to " ++ addr)

-- sendWelcomeEmail (UserId 42)   -- won't compile: UserId isn't an Email,
                                   -- even though both are "really" just Int/String underneath
In the Wild

This pattern — wrapping a primitive type in a newtype purely so the compiler can catch mix-ups — is extremely common in real Haskell codebases: UserId, Email, Age, Meters versus Feet, all wrapping ordinary Ints or Doubles or Strings, precisely so that passing an Age where a UserId was expected becomes a compile error instead of a production incident. Other languages reach for the same idea with names like “branded types” or “opaque types,” usually with far more ceremony than a one-line newtype.

Defining functions: equations and pattern matching

The most idiomatic way to define a function is as a set of equations, each matching a different shape of input — not a single body with an if inside:

describe :: Int -> String
describe 0 = "zero"
describe 1 = "one"
describe n = "some number: " ++ show n
Anatomy of a factorial function definition: type signature, base case, recursive case

Figure: factorial, taken apart. GHC tries each equation in order, top to bottom, and uses the first one whose pattern matches the actual argument.

Patterns can go well beyond literal numbers — they can take a data structure apart directly:

firstOf :: (a, b) -> a
firstOf (x, _) = x                -- _ means "I don't care about this part"

headOr :: a -> [a] -> a
headOr def []    = def            -- matches the empty list
headOr _   (x:_) = x              -- matches "at least one element"; x is that element

describePoint :: (Int, Int) -> String
describePoint (0, 0) = "origin"
describePoint (0, _) = "on the y-axis"
describePoint (_, 0) = "on the x-axis"
describePoint p@(x, y) = "point " ++ show p ++ " at (" ++ show x ++ "," ++ show y ++ ")"

That last equation uses an as-pattern, p@(x, y): it binds p to the whole tuple while simultaneously taking it apart into x and y — useful whenever you need both the pieces and the original together.

Guards: conditions between the pattern and the body

When the choice depends on a condition rather than a shape, a guard reads almost like a mathematician’s piecewise definition:

bmiCategory :: Double -> String
bmiCategory bmi
  | bmi < 18.5 = "underweight"
  | bmi < 25.0 = "normal"
  | bmi < 30.0 = "overweight"
  | otherwise  = "obese"

Each | line is checked top to bottom; the first True guard wins, and otherwise (just True under a friendlier name) catches whatever’s left. Guards and pattern-matched equations combine freely:

classify :: Int -> String
classify n
  | n < 0     = "negative"
  | n == 0    = "zero"
  | even n    = "positive and even"
  | otherwise = "positive and odd"

case expressions: pattern matching mid-expression

Equations put pattern matching at the definition level; case brings the same power inside an expression, anywhere one is needed:

describe' :: Maybe Int -> String
describe' mx = case mx of
  Nothing -> "nothing to see"
  Just 0  -> "exactly zero"
  Just n  -> "got " ++ show n

This is exactly equivalent to writing describe' as separate top-level equations on mxcase is what you reach for when the matching needs to happen partway through a larger function rather than on the whole argument list.

where and let: naming things locally

Both introduce local bindings, visible only nearby — where attaches to a whole set of guarded equations after the fact, let is an ordinary expression you can drop in anywhere:

quadraticRoots :: Double -> Double -> Double -> (Double, Double)
quadraticRoots a b c = (root1, root2)
  where
    discriminant = b*b - 4*a*c
    sqrtDisc     = sqrt discriminant
    root1        = (-b + sqrtDisc) / (2*a)
    root2        = (-b - sqrtDisc) / (2*a)

circleArea :: Double -> Double
circleArea r =
  let piApprox = 3.14159
  in piApprox * r * r

where bindings are shared across all guards of the equation they’re attached to, computed once; let ... in ... is a self-contained expression that can appear anywhere a value is expected, including nested inside another expression.

Currying, partial application, and sections

Chapter 2 goes into why this works from first principles (every Haskell function secretly takes exactly one argument); here’s the practical version. Because f :: a -> b -> c is really a -> (b -> c), supplying only the first argument gives back a perfectly good, reusable function:

add :: Int -> Int -> Int
add x y = x + y

add5 :: Int -> Int
add5 = add 5              -- partial application: supply the first argument, get a function back

ghci> add5 10
15
ghci> map add5 [1,2,3]
[6,7,8]

Sections are the same idea applied to operators, wrapping one side of an infix operator in parentheses to get a one-argument function:

ghci> map (+3) [1,2,3]      -- (+3) means "add 3 to whatever comes in"
[4,5,6]
ghci> map (3-) [1,2,3]      -- (3-) means "subtract whatever comes in, from 3"
[2,1,0]
ghci> filter (>10) [5,15,8,20]
[15,20]
Common Pitfall

(+3) and Just (+3) show up constantly once you reach Applicatives — (+3) isn’t special syntax tied to Maybe or any other type, it’s just an ordinary one-argument function (Int -> Int, via a section), and Just (+3) is simply that function wrapped in Maybe, exactly the way Just 5 wraps a number. If a section like this ever looks unfamiliar later in the book, it’s worth returning to this paragraph rather than the chapter you’re in — the confusion is almost always about sections, not about whatever new concept is being introduced.

Lambda expressions: functions with no name

Sometimes a function is only needed once, inline, and naming it would be more ceremony than it’s worth — a lambda (Chapter 2’s λ made literal in ASCII as \) creates one on the spot:

ghci> map (\x -> x * x) [1,2,3,4]
[1,4,9,16]
ghci> map (\x -> x `mod` 2 == 0) [1,2,3,4]
[False,True,False,True]

\x -> x * x and a top-level square x = x * x compute exactly the same function — the lambda just never gets a name bound to it, which is fine when it’s only ever used in one place.

Recursion: functions that call themselves

Haskell has no built-in looping construct (for, while) — repetition is expressed as a function calling itself with a smaller version of the problem, exactly as factorial did above:

sumTo :: Int -> Int
sumTo 0 = 0
sumTo n = n + sumTo (n - 1)

length' :: [a] -> Int
length' []     = 0
length' (_:xs) = 1 + length' xs

Every well-behaved recursive function needs two things: a base case that stops the recursion outright (sumTo 0, length' []), and a recursive case that makes real progress toward that base case on every call (n - 1, the shorter list xs). Miss either one and the function either never terminates or never actually computes anything.

Anecdote

Haskell doesn’t lack loops by oversight — recursion (plus the higher-order functions like map and filter you’ve already seen above) is genuinely the only repetition mechanism the language offers, and Chapter 6’s laziness is exactly what keeps this from being a limitation: an infinitely recursive definition like allPositiveIntegers = [1..] is perfectly fine, because nothing forces the recursion to actually finish.

A quick taste of common types

You’ll see these constantly starting in the very next chapter, well before their formal treatment starting in Chapter 9:

-- Maybe: a value, or the honest absence of one
safeDivide :: Int -> Int -> Maybe Int
safeDivide _ 0 = Nothing
safeDivide x y = Just (x `div` y)

-- map, filter, and fold: the three workhorses of list processing
ghci> map (*2) [1,2,3]
[2,4,6]
ghci> filter even [1,2,3,4,5,6]
[2,4,6]
ghci> foldr (+) 0 [1,2,3,4]
10

foldr (+) 0 [1,2,3,4] combines every element with (+), starting from 0, right to left: 1 + (2 + (3 + (4 + 0))). You’ll meet map again, formally, as fmap in Chapter 9 — it isn’t a coincidence that the names rhyme.

Cheat sheet

SyntaxMeaning
x :: Tx has type T
data T = A x | B y zdefine a new type T with constructors A, B
newtype T = T xa zero-cost wrapper around one field
f x = ...define f by equation, matching on x’s shape
| cond = ...a guard — checked top to bottom
case e of ...pattern match inside an expression
where ...local bindings, shared across an equation’s guards
let ... in ...local bindings, as a self-contained expression
f x ycurried application — really (f x) y
(+3), (3-)a section — one side of an operator, made into a function
\x -> ...a lambda — a function with no name
f 0 = ...; f n = ...recursion via base case + recursive case
λA Category-Theoretic View

Nothing in this chapter is Haskell-specific in spirit — pattern matching, guards, and recursion are just how you’d describe a piecewise mathematical function on paper, transcribed directly into code. That transcription-not-translation relationship between the math and the syntax is precisely this book’s central theme, and Chapter 2 makes the connection formal.

With this vocabulary in hand, every code sample from here on should read as prose, not puzzle — on to Lambda Calculus, where these same shapes turn out to be a 90-year-old idea wearing modern clothing.