You are viewing a free preview of this lesson.
Subscribe to unlock all 10 lessons in this course and every other course on LearningBro.
Function composition and partial application are the two techniques that let you build powerful, complex functions out of small simple ones — and they are where Haskell's design really pays off. Composition (the . operator) plugs functions together end-to-end; partial application (a consequence of currying) lets you fix some arguments now and supply the rest later. AQA examiners frequently combine the two with map/filter/fold in a single question, and they test whether you understand the subtle difference between currying and partial application, so precision matters here. By the end of this lesson you should be able to trace a composition right-to-left, fix arguments by partial application, and explain clearly how currying makes that possible.
This lesson covers part of AQA 7517 §4.12.2 Writing functional programs: function application, partial function application, and the use of function composition to build new functions. You should be able to: define and use the composition operator .; explain that Haskell functions are curried; perform partial application by supplying fewer arguments than a function expects; trace a composition pipeline; and distinguish currying from partial application. It builds on higher-order functions (§4.12.2) and combines naturally with map/filter/fold (§4.12.3).
The explanations here are our own restatement of the assessable content, not a verbatim quotation of AQA's specification. Worked traces and the worked examples are included to build the fluency the exam rewards.
Function composition combines two (or more) functions into a single new function by feeding the output of one straight into the input of the next.
If g : A -> B and f : B -> C, the composition f . g : A -> C is defined by:
You apply g first, then apply f to its result. The types must line up: g's output type (B) must be f's input type. This is why composition reads right to left.
. operatorIn Haskell the composition operator is the dot. Its own type signature is itself instructive — it is a higher-order function:
(.) :: (b -> c) -> (a -> b) -> (a -> c)
double x = x * 2
addOne x = x + 1
-- Compose: first double, THEN add one
doubleThenAddOne = addOne . double
doubleThenAddOne 5 -- double 5 = 10, then addOne 10 = 11
-- The opposite order gives a different function
addOneThenDouble = double . addOne
addOneThenDouble 5 -- addOne 5 = 6, then double 6 = 12
Read right to left. In
f . g, the function nearest the argument (g) runs first. This trips students up constantly —addOne . doubledoes not add one first.
(addOne . double) 5
= addOne (double 5) -- definition of (.)
= addOne 10 -- double 5 reduces to 10
= 11
negate' x = -x
double x = x * 2
addOne x = x + 1
transform = negate' . addOne . double
transform 3
= negate' (addOne (double 3))
= negate' (addOne 6)
= negate' 7
= -7
A composition pipeline is easiest to picture as data flowing right-to-left through a chain of boxes:
flowchart RL
IN["input 3"] --> D["double -> 6"]
D --> A["addOne -> 7"]
A --> N["negate' -> -7"]
N --> OUT["result -7"]
A useful property worth knowing is that function composition is associative: it does not matter how you bracket a chain of compositions, the resulting function is the same.
(f∘g)∘h=f∘(g∘h)In Haskell, (negate' . addOne) . double and negate' . (addOne . double) define the same function, which is why we can write the chain negate' . addOne . double with no brackets at all and read it unambiguously. (Note that associativity is about grouping, not order: the functions still run right to left — associativity does not let you reorder them, only re-bracket them.) This is the same associativity that makes a sequence of Unix pipes well-defined regardless of how you imagine grouping them, and it is part of why composition scales so cleanly to long pipelines.
Python has no built-in composition operator, so you build one — which highlights what Haskell gives you for free:
def compose(f, g):
return lambda x: f(g(x))
double = lambda x: x * 2
add_one = lambda x: x + 1
double_then_add_one = compose(add_one, double)
print(double_then_add_one(5)) # 11
process = sum . map (*2) rather than process xs = sum (map (*2) xs)).Composition and ordinary nested application compute the same thing; the difference is style. Writing f (g (h x)) applies the chain to a specific argument x immediately. Writing (f . g . h) builds a new function that you can name, pass around, or apply later. Compare:
-- nested calls: must mention the argument xs
clean xs = map toUpper (filter isLetter xs)
-- composition: defines a reusable function, no argument named (point-free)
clean = map toUpper . filter isLetter
Both clean "a1b2!" give "AB". The composed version is preferable when you want to treat the whole transformation as a value — for example to store it in a list of "cleaning steps" or pass it to another higher-order function. The nested version can be clearer when there is only one call site and the intermediate naming aids understanding. Neither is "more correct"; they are two spellings of f(g(h(x))).
Suppose we need a function that takes a sentence and returns the number of words longer than four letters. We can build it from three small, individually-obvious pieces:
countLongWords :: String -> Int
countLongWords = length . filter (\w -> length w > 4) . words
words splits a string on spaces into a list of words; filter (\w -> length w > 4) keeps the long ones; length counts what remains. Trace it right-to-left on "the quick brown fox jumped":
countLongWords "the quick brown fox jumped"
= length (filter (\w -> length w > 4) (words "the quick brown fox jumped"))
= length (filter (\w -> length w > 4) ["the","quick","brown","fox","jumped"])
= length ["quick", "brown", "jumped"] -- the 5+ letter words
= 3
Each function is trivial alone; composition assembles them into a non-trivial whole — the essence of building complex behaviour from simple parts.
To understand partial application you must first understand currying — and the two are not the same thing, a distinction examiners specifically test.
Currying is treating a multi-argument function as a chain of single-argument functions, each returning the next. In Haskell every function is curried automatically.
Uncurried:Curried:f(x,y,z)=x+y+zfxyz=((fx)y)zThe signature add :: Int -> Int -> Int is really add :: Int -> (Int -> Int) because -> associates to the right. So add is a function that takes one Int and returns a function Int -> Int:
add :: Int -> Int -> Int
add x y = x + y
-- add 3 on its own is a FUNCTION (Int -> Int), not yet a number
(add 3) 4 -- ((add 3) applied to 4) = 7
add 3 4 -- exactly the same; application is left-associative
In Python the same restructuring must be done by hand:
# Uncurried (takes both at once)
def add(x, y):
return x + y
# Curried (takes one, returns a function expecting the next)
def add_curried(x):
return lambda y: x + y
add_curried(3)(4) # 7
The chaining continues for any number of arguments. Consider a three-argument function and its real (fully bracketed) type:
addThree :: Int -> Int -> Int -> Int -- really Int -> (Int -> (Int -> Int))
addThree x y z = x + y + z
Applying it one argument at a time shows the chain of returned functions:
addThree 1 2 3
= ((addThree 1) 2) 3 -- application is left-associative
= (g 2) 3 -- addThree 1 returns a function g :: Int -> (Int -> Int)
= h 3 -- g 2 returns a function h :: Int -> Int
= 6 -- h 3 finally yields an Int
At each step a single argument is consumed and a smaller function is returned, until the last argument produces the final Int. This is why you can partially apply at any stage: addThree 1 is a two-argument function, addThree 1 2 is a one-argument function, and addThree 1 2 3 is the value. Currying makes every prefix of the argument list a usable function in its own right.
Partial application is supplying fewer arguments than a function expects, which yields a new function awaiting the remaining arguments. Because Haskell functions are curried, partial application is completely natural — you just leave arguments off. The result is a specialised version of the original: a general two-argument adder becomes a specific "add five" function; a general multiplier becomes a "times ten". The original definition is untouched and remains usable in full; partial application simply creates a convenient, more specific function from it on demand.
add :: Int -> Int -> Int
add x y = x + y
-- Fix the first argument to 5; addFive is a NEW function (Int -> Int)
addFive :: Int -> Int
addFive = add 5
addFive 3 -- 8
addFive 10 -- 15
multiply x y = x * y
double = multiply 2 -- partial application
triple = multiply 3
double 7 -- 14
triple 7 -- 21
-- Partial application feeds tidy one-argument functions to map/filter
map (multiply 10) [1, 2, 3, 4, 5] -- [10, 20, 30, 40, 50]
-- Operator sections are partial applications of operators
greaterThanFive = (> 5)
filter greaterThanFive [1..10] -- [6, 7, 8, 9, 10]
The expressions (*2), (>5), (+1) you have been using all along are operator sections — partial applications of the underlying binary operators.
The map (multiply 10) ... line above is the everyday reason partial application matters: map needs a one-argument function, but multiply takes two. Partially applying it (multiply 10) produces exactly the one-argument function map requires, with no lambda needed. Without currying you would have to write map (\x -> multiply 10 x) ... every time — partial application makes the common case clean. The same applies to filter (> 5), map (+ 1) and countless other pipeline stages you have already been writing.
It helps to see why add 5 is a valid function. Because add :: Int -> Int -> Int is really add :: Int -> (Int -> Int), applying it to one argument is a complete operation that returns a function. Step through add 5 3:
add 5 3
= (add 5) 3 -- application is left-associative: add 5 happens first
= (\y -> 5 + y) 3 -- add 5 returns a function awaiting y, with x fixed at 5
= 5 + 3
= 8
The intermediate value add 5 (equivalently \y -> 5 + y) is exactly what addFive names. So partial application is not a special language feature bolted on — it falls straight out of the fact that every function is curried. In an uncurried language add 5 would simply be an error ("missing argument"); in Haskell it is a perfectly good Int -> Int.
Operator sections come in two forms, and the side you leave blank is the side the argument fills:
(/ 2) 5 -- right section: 5 / 2 = 2.5 (argument goes on the LEFT of /)
(2 /) 5 -- left section: 2 / 5 = 0.4 (argument goes on the RIGHT of /)
(subtract 3) 10 -- 10 - 3 = 7 ((-3) would be negative three, so use 'subtract')
The subtraction case is a classic trap: (- 3) is read by Haskell as the number negative three, not "subtract three", so the named function subtract is used instead. Knowing which section you have written matters whenever the operator is non-commutative.
Python is not curried, so partial application needs functools.partial:
from functools import partial
def multiply(x, y):
return x * y
double = partial(multiply, 2)
result = list(map(partial(multiply, 10), [1, 2, 3, 4, 5]))
# [10, 20, 30, 40, 50]
| Concept | What it is | Example |
|---|---|---|
| Currying | A structural property: a multi-argument function is really a chain of one-argument functions. In Haskell this is automatic. | add :: Int -> Int -> Int is Int -> (Int -> Int) |
| Partial application | An act you perform: call a function with fewer arguments than it expects to obtain a specialised function. | addFive = add 5 |
Subscribe to continue reading
Get full access to this lesson and all 10 lessons in this course.