You are viewing a free preview of this lesson.
Subscribe to unlock all 10 lessons in this course and every other course on LearningBro.
Haskell is the purely functional language AQA uses to teach the functional paradigm at A-Level, so fluency in reading and writing short Haskell programs is directly examinable: questions routinely give you Haskell code to trace, or ask you to write a small function. What makes Haskell such a good teaching language is that its design forces the paradigm's discipline on you — there is no way to mutate a variable, no way to perform I/O outside a clearly-marked type, and every expression carries a type the compiler checks before the program runs.
This lesson is a guided tour of the syntax you must command: type signatures (which double as a statement of what a function does), defining functions, pattern matching, guards, where/let local definitions, tuples, type classes, and how the type system itself becomes a teaching tool — catching whole classes of error at compile time and guiding you towards a correct definition. Throughout, the emphasis is on reading code precisely, because that is the skill most exam marks hinge on.
This lesson addresses AQA 7517 §4.12, writing functional programs, using Haskell as the vehicle. You should be able to: read and write type signatures of the form name :: A -> B (including multi-argument and polymorphic-with-constraint signatures); define functions by equations; use pattern matching on literal values, lists (x:xs) and tuples; use guards for conditional logic and explain how they differ from if-then-else; introduce local definitions with where and let ... in; and recognise the role of the type system (strong, static typing) in catching errors before run time. It builds on the paradigm characteristics of §4.12.1 and supports list processing (§4.12.3); the type system links to data types in §4.1.
The descriptions here are our own restatement of the assessable content, not a verbatim quotation of AQA's specification.
| Feature | What it means | Why it matters for learning FP |
|---|---|---|
| Purely functional | No side effects; all data immutable | The paradigm's discipline is unavoidable, so you cannot "cheat" with mutation |
| Strong, static typing | Every expression has a type, checked at compile time | Whole classes of error are impossible to express, so the type system teaches you |
| Lazy evaluation | Expressions computed only when needed | Enables infinite lists and clean separation of what from when |
| Concise, mathematical syntax | Definitions read like equations | Code mirrors the maths, reinforcing "a function is a mapping" |
The combination is what makes Haskell pedagogically valuable rather than merely usable: a beginner is gently but firmly prevented from writing imperative-in-disguise code.
A Haskell function is defined by an equation, usually preceded by a type signature:
double :: Int -> Int -- type signature: maps an Int to an Int
double x = x * 2 -- definition: the body is an expression
add :: Int -> Int -> Int -- maps an Int and an Int to an Int
add x y = x + y
result = double 5 -- 10
total = add 3 4 -- 7
The first line is the type signature; the second is the definition. The signature is technically optional (Haskell can infer it) but writing it is strongly encouraged: it documents intent, localises errors, and is exactly what examiners expect to see. Read the signature as a statement of domain and co-domain — double :: Int -> Int is literally "the function double maps an integer to an integer".
square :: Int -> Int -- one Int in, one Int out
multiply :: Int -> Int -> Int -- two Ints in, one Int out
isPositive :: Int -> Bool -- an Int in, a Bool out (a predicate)
isEqual :: Eq a => a -> a -> Bool -- polymorphic with a constraint
The arrow -> separates argument types from the result and associates to the right, so Int -> Int -> Int secretly means Int -> (Int -> Int) — a fact that becomes important with currying. The last signature introduces a type variable a and a constraint Eq a =>, read "for any type a that supports equality". This lets one definition of isEqual work for Int, Char, String and more, while the constraint guarantees == is actually available for whatever a turns out to be.
| Type | Description | Example values |
|---|---|---|
Int | Fixed-precision integer | 42, -7, 0 |
Integer | Arbitrary-precision integer | 999999999999999999 |
Float / Double | Floating point (single / double) | 3.14, -0.5 |
Bool | Boolean | True, False |
Char | Single character | 'a', 'Z', '3' |
String | List of characters | "hello" |
[Int] | List of integers | [1, 2, 3] |
(Int, String) | Tuple (fixed-size, mixed type) | (42, "hello") |
Pattern matching lets you give a function several equations, each handling a different shape of input. Haskell tries the equations top to bottom and uses the first whose pattern matches.
-- Matching on literal values (the base cases come first)
fibonacci :: Int -> Int
fibonacci 0 = 0
fibonacci 1 = 1
fibonacci n = fibonacci (n - 1) + fibonacci (n - 2)
-- Matching on list structure: [] vs (head : tail)
firstOrZero :: [Int] -> Int
firstOrZero [] = 0 -- empty list
firstOrZero (x:_) = x -- non-empty: bind head to x, ignore the tail with _
-- Matching on tuples
fst' :: (a, b) -> a
fst' (x, _) = x -- bind the first component, ignore the second
snd' :: (a, b) -> b
snd' (_, y) = y
The underscore _ is a wildcard that matches anything without binding a name — use it to signal "I don't care about this part". Because equations are tried in order, base cases must come before the general case; if you wrote fibonacci n first it would match 0 and 1 too, and the specific equations would never fire.
Trace fibonacci 4 to see the equations selected at each step:
fibonacci 4
= fibonacci 3 + fibonacci 2 -- n case
= (fibonacci 2 + fibonacci 1) + fibonacci 2
= ((fibonacci 1 + fibonacci 0) + 1) + fibonacci 2 -- 1 and 0 cases fire
= ((1 + 0) + 1) + (fibonacci 1 + fibonacci 0)
= (1 + 1) + (1 + 0)
= 2 + 1
= 3
Guards attach boolean conditions to a single equation, choosing a result like an if-else chain but more readable:
bmiCategory :: Double -> String
bmiCategory bmi
| bmi < 18.5 = "Underweight"
| bmi < 25.0 = "Normal"
| bmi < 30.0 = "Overweight"
| otherwise = "Obese"
Guards are tested top to bottom; the first one that is True supplies the result. otherwise is not magic — it is simply a constant defined to equal True, so it always matches and serves as the catch-all. A subtle but examinable point: each guard returns the value, so the conditions should be ordered so that earlier ones "claim" their range — here bmi < 25.0 only ever sees values already known to be at least 18.5, because the first guard handled anything smaller.
Both express conditional logic, but they differ in shape and idiom. Guards are preferred when there are several mutually-exclusive cases, because they avoid deeply nested ifs; if-then-else suits a single binary choice inside a larger expression. Crucially, in Haskell if-then-else is an expression — it returns a value, so both branches must have the same type and an else is mandatory:
absolute :: Int -> Int
absolute n = if n >= 0 then n else -n
You could write absolute with guards instead (| n >= 0 = n | otherwise = -n); for more than two cases, guards almost always read better.
where and letBoth where and let introduce local definitions that name sub-results, improving readability and avoiding repetition.
where — definitions placed after the expression that uses them:
cylinderArea :: Double -> Double -> Double
cylinderArea r h = sideArea + 2 * baseArea
where
sideArea = 2 * pi * r * h
baseArea = pi * r^2
let ... in — definitions placed before the expression, as part of it:
cylinderArea' :: Double -> Double -> Double
cylinderArea' r h =
let sideArea = 2 * pi * r * h
baseArea = pi * r^2
in sideArea + 2 * baseArea
The two are largely interchangeable; the main differences are scope and placement. A where clause attaches to a whole definition (and can span several guards, which is a common reason to prefer it), whereas let ... in is itself an expression and can appear anywhere an expression is allowed. Both let you name baseArea once and reuse it, and — because everything is immutable — those names are permanent equalities, not reassignable variables.
A tuple holds a fixed number of elements of possibly different types, in contrast to a list (variable length, single type):
point = (3, 4) :: (Int, Int)
person = ("Alice", 25) :: (String, Int)
fst (3, 4) -- 3
snd (3, 4) -- 4
| Feature | List | Tuple |
|---|---|---|
| Length | Variable | Fixed by its type |
| Element types | All identical | May differ per position |
| Example | [1, 2, 3] | (1, "hello", True) |
Tuples are how you return more than one value from a function (Haskell has no "out" parameters), and they pattern-match cleanly, as fst'/snd' above showed.
dataBeyond the built-in types, you can declare your own with the data keyword, naming the type and listing its constructors (the ways a value of that type can be built). The simplest case is an enumeration:
data Suit = Hearts | Diamonds | Clubs | Spades
A constructor may also carry data, which is how you model structured values:
data Shape = Circle Double -- a circle described by its radius
| Rectangle Double Double -- a rectangle described by width and height
The real power appears when you pattern-match on the constructors — each equation handles one shape of value, binding the carried data to names:
area :: Shape -> Double
area (Circle r) = pi * r * r
area (Rectangle w h) = w * h
area (Circle 5) -- 78.5398...
area (Rectangle 3 4) -- 12.0
This unifies two ideas you have already met. The list type is itself just such a definition ([] and (:) are its constructors), which is exactly why (x:xs) pattern matching works. Defining Shape and matching on Circle/Rectangle is the same mechanism applied to your own type — and it is how Haskell models the "kinds of thing" that an object-oriented language would model with subclasses, but checked exhaustively at compile time.
case ExpressionsPattern matching is not restricted to the tops of function definitions; the case expression lets you match inside any expression. It is useful when the value to inspect is computed locally:
describeList :: [a] -> String
describeList xs = "The list is " ++ case xs of
[] -> "empty."
[_] -> "a singleton."
(_:_) -> "longer than one element."
A case is exactly equivalent to defining the function by multiple equations — it simply moves the matching into the expression. The same ordering rule applies: branches are tried top to bottom, and a case should normally be exhaustive (cover every possible shape), or Haskell will warn that a value could slip through unmatched, risking a run-time error. Choosing between top-level equations, guards and case is a matter of readability: equations suit whole-function dispatch, guards suit boolean conditions, and case suits matching a value produced in the middle of a computation.
Haskell uses indentation rather than braces or keywords to delimit blocks — the so-called layout or offside rule. Definitions in a where, the bindings in a let, and the branches of a case are grouped by lining them up in the same column:
roots :: Double -> Double -> Double -> (Double, Double)
roots a b c = (x1, x2)
where
d = sqrt (b*b - 4*a*c) -- these three local
x1 = (-b + d) / (2*a) -- definitions are grouped
x2 = (-b - d) / (2*a) -- because they share a column
The practical consequence for the exam is that indentation is significant: misaligning the where definitions, or indenting a continuation line incorrectly, is a syntax error, not a cosmetic nicety. When you write Haskell by hand, keep grouped definitions vertically aligned. This is the same discipline as Python's significant whitespace, applied to local-definition blocks.
A feature that surprises newcomers is that, in Haskell, every function of "several arguments" is really a chain of single-argument functions — a representation called currying. The signature add :: Int -> Int -> Int is read with -> associating to the right as Int -> (Int -> Int): add takes one Int and returns a function that takes the next Int. Function application is therefore left-associative, so add 3 4 means (add 3) 4:
Subscribe to continue reading
Get full access to this lesson and all 10 lessons in this course.