-
Notifications
You must be signed in to change notification settings - Fork 4
0095: Email Tutorials – Recursive Functions in Haskell (Beginner‐Friendly)
- Why recursion matters in Haskell
- Two ways to define factorial: using library functions vs recursion
- How Haskell evaluates expressions (“turning the handle”)
- What recursion is: base case + recursive case
- Factorial by recursion (including divergence on negatives)
- Recursion on lists: the key patterns
[]andx:xs - Defining
productrecursively on lists - Defining
lengthrecursively (and why_appears) - Defining
reverserecursively (and why[x]is needed) - Recursion with multiple arguments:
zip,drop,(++) - Bringing it all together: quicksort (recursion + list comprehensions)
- Exercises you should try now
- Glossary
In a functional language like Haskell, a huge amount of programming is about defining functions that call themselves to solve smaller versions of the same problem. That idea—recursion—becomes your “looping mechanism,” especially when you work with lists.
The lecture’s big message is: recursion is not only for numbers; it’s also a natural way to process lists (and later, trees and other data). ([youtube.com][1])
Mathematically, factorial means “multiply all numbers from 1 to n.”
In Haskell you can express that directly:
factorial :: Int -> Int
factorial n = product [1..n]-
[1..n]builds the list1,2,3,...,n -
productmultiplies every element in the list
This style is short and clear.
You can also define factorial in terms of itself:
factorial :: Int -> Int
factorial 0 = 1
factorial n = n * factorial (n - 1)This has the classic structure:
- a base case (stops recursion)
- a recursive case (reduces the problem size)
A helpful way to understand recursion is to watch evaluation step-by-step: computation proceeds by applying functions to arguments.
Using the “product list” version:
factorial 4
= product [1..4]
= product [1,2,3,4]
= 1 * 2 * 3 * 4
= 24The key idea: you often need to evaluate inner parts first (like expanding [1..4]) before an outer function (like product) can finish.
A recursive function is defined in terms of itself.
Almost every beginner-friendly recursive definition follows this shape:
- Base case: “If the input is the simplest possible form, return an immediate answer.”
- Recursive case: “Otherwise, reduce the input and call the function again.”
If you forget the base case, or you reduce the input the wrong way, the function can run forever.
Because 1 is the identity for multiplication:
-
1 * x = xandx * 1 = x
So when the recursion reaches 0, returning 1 makes the whole multiplication chain work out correctly.
factorial 3
= 3 * factorial 2
= 3 * (2 * factorial 1)
= 3 * (2 * (1 * factorial 0))
= 3 * (2 * (1 * 1))
= 6With this definition, factorial (-1) never reaches the base case:
factorial (-1) = (-1) * factorial (-2)factorial (-2) = (-2) * factorial (-3)- …and so on forever
This is called diverging (not terminating). Also, in Haskell you usually write factorial (-1) with parentheses so the - isn’t misread as subtraction from something else.
For lists, the “standard patterns” are:
-
[](empty list) → base case -
(x:xs)(non-empty list) → recursive case wherexis the head andxsis the tail
This mirrors the “0 / n” pattern you saw for natural-number recursion.
Here is the classic recursive version of product:
product' :: Num a => [a] -> a
product' [] = 1
product' (n:ns) = n * product' nsWhy the base case is 1:
- If you used
0, then any multiplication would collapse to 0, giving nonsense answers.
Turning the handle on [2,3,4]:
product' [2,3,4]
= 2 * product' [3,4]
= 2 * (3 * product' [4])
= 2 * (3 * (4 * product' []))
= 2 * (3 * (4 * 1))
= 24Length does not care what the elements are, only how many.
length' :: [a] -> Int
length' [] = 0
length' (_:xs) = 1 + length' xsWhy _?
-
_is a wildcard: “there is an element here, but I don’t need its value.”
Turning the handle on [1,2,3]:
length' [1,2,3]
= 1 + length' [2,3]
= 1 + (1 + length' [3])
= 1 + (1 + (1 + length' []))
= 1 + (1 + (1 + 0))
= 3A simple recursive reverse:
reverse' :: [a] -> [a]
reverse' [] = []
reverse' (x:xs) = reverse' xs ++ [x]Explanation:
- Reverse the tail
xs - Then append the head
xto the end -
[x]is a singleton list, needed because(++)joins lists to lists (not a single value to a list).
Turning the handle on [1,2,3]:
reverse' [1,2,3]
= reverse' [2,3] ++ [1]
= (reverse' [3] ++ [2]) ++ [1]
= ((reverse' [] ++ [3]) ++ [2]) ++ [1]
= (([] ++ [3]) ++ [2]) ++ [1]
= [3,2,1]Pairs corresponding elements; stops when either list ends:
zip' :: [a] -> [b] -> [(a,b)]
zip' [] _ = []
zip' _ [] = []
zip' (x:xs) (y:ys) = (x,y) : zip' xs ysRemove n elements from the front:
drop' :: Int -> [a] -> [a]
drop' 0 xs = xs
drop' _ [] = []
drop' n (_:xs) = drop' (n-1) xsNotice two base cases:
- dropping 0 → done
- dropping from empty list → return empty list (safe behavior)
Concatenate two lists by recursing on the first list:
(++) :: [a] -> [a] -> [a]
[] ++ ys = ys
(x:xs) ++ ys = x : (xs ++ ys)This definition explains why ++ “walks along” the left list, copying its elements, then finally points to ys.
Quicksort in Haskell is a famous “small but complete” program:
qsort :: Ord a => [a] -> [a]
qsort [] = []
qsort (x:xs) =
qsort smaller ++ [x] ++ qsort larger
where
smaller = [a | a <- xs, a <= x]
larger = [b | b <- xs, b > x]What it’s doing:
-
Base case: empty list is already sorted.
-
Recursive case:
- pick pivot
x - split the rest into
smallerandlargerusing list comprehensions - recursively sort both sides
- glue together: sorted smaller + pivot + sorted larger
- pick pivot
This example is included in the recursion lecture as a “now you can fully understand it” moment. ([youtube.com][1])
Try defining these recursively (don’t use the Prelude versions while practicing):
and :: [Bool] -> Boolconcat :: [[a]] -> [a]replicate :: Int -> a -> [a]-
(!!)list indexing elem :: Eq a => a -> [a] -> Bool
Then try the “sorted lists” challenge:
-
merge :: Ord a => [a] -> [a] -> [a](merge two sorted lists) -
msort :: Ord a => [a] -> [a](merge sort)
If you want guided practice, the recursion practice page is a good companion. ([HPM Education][3])
Recursion: Defining a function in terms of itself.
Base case: The case that stops recursion (e.g., factorial 0 = 1, length [] = 0).
Recursive case: The case that reduces the input and calls the function again.
Diverge / diverging: The function never terminates (often because the base case is never reached).
Evaluate / evaluation: Computing the result of an expression by applying functions to arguments (“turning the handle”).
Pattern matching: Splitting cases by matching shapes like 0 vs n, or [] vs (x:xs).
Empty list []: A list with no elements; usually the base case for list recursion.
Cons (:): Builds a non-empty list: x:xs.
Tail: Everything after the first element (the xs in x:xs).
Wildcard _: “There is something here but I don’t need its value.”
Singleton list [x]: A list containing exactly one element, often used with (++).
Ord a => constraint: Means values of type a can be ordered (needed for sorting).
Bernard Sibanda is a global Technology Entrepreneur, Web3 and Software Consultant with a deep focus on Cardano Blockchain, Midnight and Community building.
Key Positions:
- Founder, CTO, Developer Advocate cohort #1, Fullstake Developer, Cardano Ambassador, Catalyst Project Manager, DREP-WIMS:
- Co-founder of ABL Tech and Cardano Africa Live
- EBU-certified Plutus Pioneer (Plutus/Haskell)
- Cohort #1 Plutus Pioneer Developer
- Catalyst Community Reviewer & Funded Projects Manager
-
DRep for WIMS-Cardano (ID:
drep1yguj8zu48n99pv70yl6ckzt9hdgjy8yjnlqs2uyzcpafnjgu4vkul) - Intersect Developer Advocate
- Intersect Committe Member 2025-2026
- Cardano Marketer,Promoter and blogger
- Cardano Open Source Contributor
- Cardano communities and events organizer and builder
- Cardano Ambassador for South Africa
Official links:
- Stablecoins Dex
- Coxygen Global Universities
- WIMS Cardano Global
- Cardano Africa Live
- WIMS Cardano Videos
- Cardano Smart Contract Videos
- Fullstack IT Consulting
Social links: