Functional programming: compose functions and process lists
Use original Python examples to explore the functional paradigm, with explicit connections to AQA's mathematical notation. A language supporting several paradigms can still be used to write a functional solution.
Content owner: Michael Print · Written for A-Level learners · Checked against official specifications
The idea to start with
Functional programming describes a calculation through functions and their composition. A function can be a value: assign it to a name, pass it to another function or return it. A higher-order function receives a function, returns one, or does both.
Map transforms each element, filter keeps elements satisfying a predicate, and fold combines elements into an accumulated result. Partial application fixes some inputs and produces a function for the remaining inputs; composition feeds one function's result into the next.
AQA 7517 · 4.12.1.1–4.12.1.5, 4.12.2.1, 4.12.3.1
Before you start
Useful foundations
Functions, arguments and return values
Lists and integer arithmetic
Basic set and function notation
By the end, you should be able to
Interpret domain, co-domain, function application and function types.
Pass and return functions as first-class values.
Trace partial application and composition in the correct order.
Use map, filter, fold and the specified list operations without changing the source list.
A function, its inputs and its outputs
Write f: A → B for a function with inputs in domain A and outputs in co-domain B. For f(n) = 2n, let A = {0, 1, 2} and B = {0, 1, 2, 3, 4}.
The actual outputs are {0, 2, 4}. The co-domain names permitted outputs; every permitted value need not be produced. Function application supplies an input: f(2) gives the output 4.
In Python, double refers to a function object; double(5) applies it and returns a number. Assigning operation = double creates another name for the callable, without evaluating it or copying a result.
A higher-order helper can receive operation and apply it to several values without knowing the particular transformation in advance. Functions can also be returned, as make_adder demonstrates below.
Read multi-input function types
A two-input calculation can have type add: Integer × Integer → Integer, with an ordered pair as input. The curried type Integer → (Integer → Integer) instead takes one integer and returns a function waiting for another.
Arrow types associate to the right when brackets are omitted. Supplying the only input to a one-argument function gives a result; supplying just some inputs to a multi-input calculation can produce another function.
For volume: Number → Number → Number → Number, fixing length 2 leaves width and height to supply. Also fixing width 3 leaves a function taking height and returning six times that height.
Fix inputs, then compose compatible functions
make_adder(amount) returns a function that adds the captured amount to its later input. add_three = make_adder(3) fixes one addition input; add_three(7) returns 10. The nested function supplies currying behaviour explicitly: ordinary Python functions are not automatically curried.
If f maps A to B and g maps B to C, g ∘ f maps A to C and applies f first. The intermediate output must be acceptable to the next function; a string cannot silently feed an integer-only calculation.
Reversing the functions changes the result: with f(x) = x + 3 and g(x) = 2x, (f ∘ g)(5) doubles first, then adds 3, giving 13.
Prefer functions whose results depend on their inputs and which do not modify outside state. Python supports this style but also permits side effects: using lambda or map alone does not establish purity.
Composition g ∘ f: work from the inside out
1
Input 5
Supply 5 to f(x) = x + 3.
2
Apply f first
f(5) = 8. This intermediate value becomes the input to g.
3
Apply g second
g(8) = 2 × 8 = 16. The composed result is 16.
Map, filter and fold have different jobs
The three operations answer different questions: filter decides which elements to keep, map transforms each retained element, and fold combines elements into an accumulated result. None requires changing the source list.
In Python 3, map and filter return iterators; this example uses list to display and reuse their values. Other functional languages may return lists directly. functools.reduce provides the combining operation.
Here the combining arguments are accumulator first, next element second. The explicit initial value defines empty-input behaviour. Addition starts at 0, then becomes 6, then 30.
Direction can matter. Left-fold subtraction of [3, 2] from initial 10 gives (10 − 3) − 2 = 5. A combining function need not be associative, so changing the fold direction can change its result.
Sensor readings through a functional pipeline
1
Start: [2, 5, 8, 11]
These are the original readings; the pipeline preserves this list.
2
Filter: [2, 8]
Keep the readings satisfying the even-number predicate.
3
Map: [6, 24]
Triple each surviving reading.
4
Fold: 30
Start at 0 and add each transformed reading: 0 + 6 + 24.
A list’s tail is another list
For [6, 24, 9], head is the element 6 and tail is the list [24, 9]. A singleton [6] has tail []; an empty list has no head. Check non-emptiness instead of inventing an element.
The second example constructs []; tests emptiness; obtains length, head and tail; and prepends or appends an element. Each transformation constructs a new list, leaving the original unchanged.
Python slicing and concatenation copy list contents. They demonstrate the required behaviour, without promising the constant-time operations that a persistent linked-list implementation might provide.
Worked example
Run the original sensor transformation and function composition
Filter 2, 5, 8, 11 by an even-number predicate. Only 2 and 8 satisfy it.
Map a tripling function over those two values, giving [6, 24]. Fold left with accumulator 0: 0 + 6 = 6, then 6 + 24 = 30.
make_adder returns a function. add_three is that function as a first-class value; compose receives it along with the doubling function.
The composed function first adds 3 to 5, then doubles 8 to give 16. The source readings remain [2, 5, 8, 11].
from functools import reduce
def make_adder(amount):
return lambda value: value + amount
def compose(after, before):
return lambda value: after(before(value))
readings = [2, 5, 8, 11]
even = list(filter(lambda value: value % 2 == 0, readings))
tripled = list(map(lambda value: value * 3, even))
total = reduce(lambda acc, value: acc + value, tripled, 0)
add_three = make_adder(3)
double_after_add = compose(lambda value: value * 2, add_three)
print(even)
print(tripled, total)
print(double_after_add(5))
print(readings)
Worked example
Implement and check the complete list-operation set
empty_list returns a new empty list. is_empty tests whether a list has no elements; length uses its size.
head and tail require a non-empty list. For [6, 24, 9], they return 6 and [24, 9].
Prepending 1 gives [1, 6, 24, 9]; appending 1 gives [6, 24, 9, 1]. Both preserve the original list.
For a singleton, tail returns []; requesting head of [] raises ValueError by the chosen contract. Folding an empty list with addition and initial 0 returns 0.
Exact output of functional_lists.py
Line
Output
1
6 [24, 9] 3
2
[1, 6, 24, 9] [6, 24, 9, 1]
3
True []
4
[6, 24, 9]
functional_lists.py — Python 3, operations returning new listsPython
6 original questions total 24 marks. Attempt each before opening the independently written indicative marking guidance.
Question 1
3 marks
For f(n) = 3n, domain {0, 1, 2} and co-domain {0, 1, 2, 3, 4, 5, 6}, give the actual outputs and explain why the co-domain can contain other values.
Show solution and marking guidance+
Indicative answer
The actual outputs are {0, 3, 6} (1). The co-domain specifies permitted output values (1), not a requirement that every member must be produced (1).
Question 2
4 marks
Explain two ways functions are first-class values in make_adder and compose. State why each helper is higher-order.
Functions for this question
make_adder(amount) returns lambda value: value + amount. The statement add_three = make_adder(3) stores what the helper returns.
compose(after, before) returns lambda value: after(before(value)). A caller supplies two functions as after and before.
Show solution and marking guidance+
Indicative answer
make_adder returns a function that can be assigned to a variable (1). compose receives functions as arguments (1). make_adder is higher-order because it returns a function (1); compose is higher-order because it receives and returns functions (1).
Question 3
4 marks
Let f(x) = x + 4 and g(x) = 3x. Calculate (g composed with f)(2) and (f composed with g)(2), showing the order.
Show solution and marking guidance+
Indicative answer
f first gives 6, then g gives 18 (2). g first gives 6, then f gives 10 (2). One mark for the correct intermediate step and one for each final result.
Question 4
4 marks
Filter [1, 4, 6, 9] for values greater than 4, map squaring over the result, then left-fold addition with initial 0. Give each intermediate result and the fold accumulator values.
Show solution and marking guidance+
Indicative answer
Filter gives [6, 9] (1). Map gives [36, 81] (1). The accumulator becomes 36 after the first value (1), then 117 (1).
Question 5
4 marks
A curried volume function has type Number → Number → Number → Number. Explain the result of fixing length 2 and width 5, then applying height 3. Distinguish partial application from the final application.
Show solution and marking guidance+
Indicative answer
Fixing two inputs leaves a function taking the remaining height (1), mapping it to ten times that value (1). Applying height 3 gives 30 (1). The first operation produces a function through partial application; the last supplies the final input and produces a number (1).
Question 6
5 marks
For [8], give head, tail and length using these functions. State the result of head([]), and explain why append leaves its input unchanged.
List-function definitions
head(values) and tail(values) first raise ValueError if values is empty. Otherwise head returns values[0], while tail returns values[1:].
Head is 8 (1), tail is [] (1), and length is 1 (1). head([]) raises ValueError (1). append uses concatenation to construct a new list instead of modifying the original (1).
Specification and references
This guide addresses AQA 7517 4.12.1.1–4.12.1.5, 4.12.2.1, 4.12.3.1. Check your examination year and the complete specification for the assessment scope.
These are independently written explanations and practice questions. CompSciTutoring.co.uk is not affiliated with or endorsed by an examination board. The marking guidance is indicative; always check the syllabus for your examination year.