3.1 Tuples — fixed-size, mixed-type packages
A tuple groups a fixed number of values that may have different types:
(3, "三", True) :: (Int, String, Bool)
("Asha", 19) :: (String, Int)
For pairs, the standard projection functions are fst and snd:
ghci> fst ("Asha", 19)
"Asha"
ghci> snd ("Asha", 19)
19
Tuples are how a Haskell function "returns multiple values":
divMod' :: Int -> Int -> (Int, Int)
divMod' a b = (a `div` b, a `mod` b)
3.2 Lists — variable length, single type
A list holds any number of values of the same type:
[1, 2, 3, 4] :: [Int]
['h', 'i'] :: [Char] -- same as "hi"
[(1,"one"), (2,"two")] :: [(Int, String)]
[[1,2],[3]] :: [[Int]]
The most important fact about lists: every list is built from two constructors —
[]— the empty list ("nil");x : xs— "cons": elementxstuck on the front of listxs.
So [1,2,3] is sugar for 1 : (2 : (3 : [])). This structure is exactly what pattern matching (Unit 2) takes apart.
Handy built-ins to know now: head, tail, length, reverse, ++ (append), take, drop, elem, sum, product, and ranges like [1..10] and [2,4..20].
3.3 Control structures, the functional way
There are no for/while loops. Haskell's "control structures" are:
| Need | Haskell tool | |
|---|---|---|
| Two-way branch | if cond then e1 else e2 (an expression!) | |
| Multi-way branch | guards ` | cond = ...` |
| Branch by shape of data | pattern matching / case ... of | |
| Repetition | recursion | |
| "Do this to every element" | higher-order functions: map, filter, foldr |
case expression example:
describe :: Int -> String
describe n = case n of
0 -> "zero"
1 -> "one"
_ -> "many" -- _ is the wildcard pattern
3.4 Input / Output — why it needs special treatment
A pure function cannot "read the keyboard": its result would then depend on something other than its arguments, breaking referential transparency. Haskell's solution: I/O actions are values of the special type IO a — descriptions of side-effecting actions that produce an a when executed.
| Action | Type | Meaning |
|---|---|---|
putStr s, putStrLn s | String -> IO () | print a string (with newline) |
getLine | IO String | read one line from the keyboard |
print x | Show a => a -> IO () | print any showable value |
Actions are sequenced with do notation:
main :: IO ()
main = do
putStrLn "What is your name?"
name <- getLine -- run getLine, bind result to name
putStrLn ("Hello, " ++ name ++ "!")
putStrLn "Enter a number:"
s <- getLine
let n = read s :: Int -- read: String -> Int conversion
print (n * n)
Key points the examiner checks:
name <- getLineruns an action and captures its result;let n = ...is just a local pure definition (no<-).readconverts aStringto a number;showconverts a value to aString.- The pure world (
Int -> Int) and the impure world (IO) are separated by the type system itself — you can always tell from a function's type whether it can perform I/O. This is the famous Haskell slogan: purity is enforced, not requested.
3.5 Putting it together — the evaluation pipeline
Well-designed Haskell programs keep a thin I/O shell around a large pure core — the same architecture (ports-and-adapters) that modern backend engineering recommends.
3.6 Tuples vs lists — the comparison the exam expects
| Property | Tuple | List |
|---|---|---|
| Length | fixed at compile time | any length, even infinite |
| Element types | may all differ | must all be the same |
| Type examples | (Int, String), (Bool, Char, Int) | [Int], [String] |
| Length is part of the type? | yes — (1,2) and (1,2,3) have different types | no — [1,2] and [1,2,3] share [Int] |
| Taken apart by | fst/snd or pattern (x, y) | head/tail or pattern (x:xs) |
| Typical use | one record-like value | a collection to iterate over |
Rule of thumb: a tuple answers "and" (a name and an age); a list answers "many" (many ages).
3.7 More list machinery you should recognise on sight
ghci> [1..6] -- arithmetic sequence
[1,2,3,4,5,6]
ghci> [1,3..11] -- with a step
[1,3,5,7,9,11]
ghci> ['a'..'e']
"abcde"
ghci> take 3 (repeat 0) -- repeat: infinite copies
[0,0,0]
ghci> replicate 4 'x'
"xxxx"
ghci> splitAt 2 [1,2,3,4]
([1,2],[3,4])
ghci> words "to be or not"
["to","be","or","not"]
ghci> unwords ["to","be"]
"to be"
ghci> lines "one\ntwo"
["one","two"]
words/unwords and lines/unlines are the bridge between raw input text and list processing — almost every I/O exercise uses them.
3.8 A complete interactive program — worked example
"Read n, then read n numbers, print their average." The pure part is one line; everything else is shell:
average :: [Double] -> Double -- PURE core
average xs = sum xs / fromIntegral (length xs)
main :: IO ()
main = do
putStrLn "How many numbers?"
s <- getLine
let n = read s :: Int
xs <- getNumbers n -- impure shell
putStrLn ("Average = " ++ show (average xs))
getNumbers :: Int -> IO [Double]
getNumbers 0 = return [] -- return wraps a pure value in IO
getNumbers n = do
s <- getLine
rest <- getNumbers (n - 1)
return (read s : rest)
Two points the examiner checks here:
returnis not a jump! It is an ordinary functionreturn :: a -> IO athat builds an action producing a value and doing nothing else. Code after it still runs.- Repetition in
IOis done by recursion on a counter (getNumbers), exactly like pure recursion — there is still no loop statement.
3.9 case vs guards vs patterns — choosing the right branch tool
| Situation | Best tool |
|---|---|
| Decide by the shape/constructor of data | pattern matching in equations |
| Decide by a boolean condition on values | guards |
| Pattern-match in the middle of an expression | case ... of |
| Simple two-way value choice | if ... then ... else |
These compile to the same thing; the choice is purely about readability — but exams do ask you to rewrite one form into another, so practise converting the grade example between guards and if chains.
Exam pointers
- "Why does I/O need special treatment in a pure language?" — answer with: referential transparency (§1.7 of the paradigm lesson) would break; therefore actions are values of type
IO a, sequenced bydo; purity is visible in types. - "Differentiate tuple and list" — reproduce the table in §3.6 with one example each.
- A
do-block program reading input and printing a computed result (like §3.8) is a standard 5-mark write-up; keep the pure function separate to collect design marks.
Self-check
- What is the type of
("roll", 42, True)? And of[("a",1), ("b",2)]? - Why is
name <- getLinenot the same aslet name = getLine? - Write
swap :: (a, b) -> (b, a)using a pattern. - Desugar
[1,2,3]into cons applications. - What does
return ()do inside adoblock — and what does it not do?