1.1 What is functional programming?
In a functional language, a program is built entirely from expressions and function definitions. Computation means evaluating an expression to a value — never executing commands that overwrite memory.
| Imperative thinking | Functional thinking |
|---|---|
| Variables are boxes you can overwrite | Names are bound to values once, forever |
| Loops repeat commands | Recursion repeats computation |
x = x + 1 is normal | x = x + 1 is a contradiction (no solution!) |
| Order of statements matters | Order of definitions does not matter |
| Functions may have side effects | Pure functions: output depends only on input |
1.2 Key properties of functional languages
- Purity / no side effects. Calling
f 5today, tomorrow, or twice in a row always gives the same answer. This property is called referential transparency — any expression can be replaced by its value without changing the program's meaning. - First-class functions. Functions are values: they can be passed as arguments, returned as results and stored in lists. A function that takes or returns functions is called a higher-order function.
- Lazy evaluation (Haskell-specific). An expression is evaluated only when its value is actually needed. This allows infinite data structures like
[1..](the list of all positive integers). - Strong static typing. Every expression has a type known at compile time; type errors are caught before the program runs.
1.3 Getting started with GHC and GHCi
GHC (Glasgow Haskell Compiler) is the standard Haskell compiler. GHCi is its interactive shell (REPL) — you type expressions, it evaluates them.
-- Start the REPL by typing: ghci
ghci> 2 + 3 * 4
14
ghci> (2 + 3) * 4
20
ghci> not True
False
ghci> "hello " ++ "world"
"hello world"
Useful GHCi commands (all start with a colon):
| Command | What it does |
|---|---|
:load myfile.hs (or :l) | Load a script file |
:reload (or :r) | Reload after editing the file |
:type expr (or :t) | Show the type of an expression |
:info name (or :i) | Show information about a name or type class |
:quit (or :q) | Exit GHCi |
1.4 Your first script
Save this as First.hs and load it with :l First.hs:
-- First.hs : our first Haskell definitions
square :: Int -> Int -- type signature: Int to Int
square x = x * x -- definition: an EQUATION, not a command
areaOfCircle :: Double -> Double
areaOfCircle r = pi * r * r
greet :: String -> String
greet name = "Hello, " ++ name ++ "!"
ghci> square 7
49
ghci> areaOfCircle 2.0
12.566370614359172
ghci> greet "Asha"
"Hello, Asha!"
Notice what is missing: no return, no braces, no semicolons, no parentheses around arguments. square 7 — function application is written by juxtaposition (a space) and binds tighter than any operator, so square 3 + 1 means (square 3) + 1 = 10.
1.5 Function application vs operators
f x yappliesfto two arguments.- Operators like
+are just functions with symbolic names;(+) 2 3is the same as2 + 3. - A two-argument function can be used as an infix operator by wrapping its name in backquotes:
7 `div` 2 -- infix form: exactly the same as div 7 2 → 3
1.6 The evaluation model: rewriting
Haskell evaluates by rewriting expressions using the defining equations, just like simplifying in algebra:
square (3 + 1)
= square 4 -- arithmetic (or later, if lazy)
= 4 * 4 -- by the equation square x = x * x
= 16
This "calculation" view is the heart of the whole subject — the lambda-calculus lesson makes it formal, and the graph-reduction lesson shows how the machine actually does it.
1.7 Referential transparency — the exam definition
Definition. An expression is referentially transparent if it can be replaced by its value (or by any other expression equal to it) anywhere in the program without changing the program's meaning. A language is referentially transparent when every expression in it has this property.
Why imperative code breaks it — a side-by-side you can reproduce in the exam:
// C-like, with hidden state: -- Haskell, pure:
int c = 0; f :: Int -> Int
int f(int x) { f x = x + 1
c = c + 1;
return x + c; -- f 5 is ALWAYS 6, so
} -- f 5 + f 5 == 2 * f 5 ✓ holds
// f(5) + f(5) = 6 + 7 = 13
// 2 * f(5) = 2 * 6 = 12 → NOT equal! equational reasoning fails
Consequences worth listing in an answer: (1) the compiler may evaluate sub-expressions in any order; (2) repeated expressions can be computed once and shared; (3) expressions can safely run in parallel; (4) programs can be verified by substituting equals for equals, exactly like algebra.
1.8 Eager vs lazy evaluation — first taste
The same expression can be reduced in two different orders. Both reach the same answer here (the Church–Rosser theorem of the lambda-calculus lesson guarantees answers never disagree), but the work done differs:
Applicative order (eager — C, Java, Python): Normal order + sharing (lazy — Haskell):
square (3 + 1) square (3 + 1)
= square 4 -- argument first = x * x where x = (3+1) -- ONE shared node
= 4 * 4 = 4 * 4 -- forced once
= 16 = 16
Where laziness visibly changes behaviour:
ghci> fst (1, undefined) -- lazy: undefined never touched
1
ghci> take 3 [1..] -- works on an INFINITE list
[1,2,3]
An eager language would crash on the first and loop forever on the second. Remember the slogan: lazy evaluation computes a value at most once, and only if it is needed.
1.9 Advantages and limitations of functional programming (classic 5-mark question)
Advantages
- Easier reasoning and testing — referential transparency means a function is fully specified by its input/output table; no hidden state to set up.
- Conciseness — higher-order functions and recursion replace boilerplate loop code (quicksort in ~3 lines, Unit 2).
- Fewer bugs by construction — no aliasing, no race conditions on shared mutable state; strong static types catch errors at compile time.
- Free parallelism opportunities — independent pure expressions can be evaluated concurrently.
- Mathematical foundation — programs can be proved correct by induction (Unit 2) because programs are equations.
Limitations
- I/O and state need special machinery (
IOactions, Unit 1's data & I/O lesson) — less direct than imperative assignment. - Performance can be harder to predict — laziness may build up unevaluated expressions (space leaks, discussed with graph reduction).
- Algorithms that are naturally in-place (e.g. array sorting) need different formulations or extra copying.
- A genuinely different mental model — the learning curve is the price of the benefits.
Exam pointers
- "Define referential transparency / pure function / higher-order function" — quote the boxed definition and give the C-vs-Haskell counter-example above.
- "Differentiate imperative and functional programming" — use the table in §1.1; add
x = x + 1as the punchline. - Hand-evaluation of a small definition (like §1.6) is a near-certain question — write every rewrite step, one per line, with the rule used.
Self-check
- Why is
x = x + 1meaningless as a Haskell equation but normal in C? - State two compiler optimisations that referential transparency makes safe.
- Evaluate
square (square 2)by hand, showing every step. - Which GHCi command shows the type of an expression? Which reloads a file?
- Give one expression that terminates lazily but would fail under eager evaluation.