Module 02.2: Functions = Values¶
Module 02.1 gave us the basic Haskell machinery: expressions, bindings, types, function definitions, and a first encounter with functional programming. Now we can ask what that programming model actually buys us.
This module develops one central idea:
In Haskell, functions are values.
Getting there will take us through several ideas that initially seem separate: purity, lazy evaluation, operator precedence, currying, higher-order functions, and function composition. By the end, they will fit together.
By the end of this module, you should be able to:
- distinguish syntax errors, type errors, logic errors, and other language-rule violations,
- explain purity, side effects, and referential transparency,
- compare eager and lazy evaluation,
- predict whether simple lazy expressions terminate or diverge,
- use precedence and associativity to determine how Haskell groups an expression,
- explain how ordinary operators and functions are related in Haskell,
- explain why every Haskell function can be understood as taking one argument,
- read a curried function type in more than one equivalent way,
- explain higher-order and first-class functions,
- recognize partial application and simple point-free definitions, and
- use function composition to build larger functions from smaller ones.
How to use this module
When you see a box labeled Gradescope question, answer that question in the Module 2.2 Completion assignment on Gradescope.
When you see Try it in ghci or Pause and predict, the activity is there to help you understand the material. There is nothing to submit for those boxes unless the box explicitly says Gradescope question.
Warmup: What Kind of Wrong Is This?¶
Before adding new Haskell ideas, it is useful to sharpen a skill from Module 02.1: recognizing what kind of problem Haskell is reporting.
These categories are different:
- A syntax error or parse error means Haskell cannot interpret the text as a well-formed expression or definition.
- A type error means the expression has valid syntax, but its pieces have incompatible types.
- A logic error means the program is valid and runs, but it computes something different from what the programmer intended.
- Some errors come from other language rules. For example, two definitions can each be syntactically and type-correct on their own while still violating Haskell's rule against defining the same name twice in one scope.
The point is not to memorize labels. The useful habit is to ask: How far did Haskell get before things went wrong?
Warmup 1¶
Gradescope question: What kind of error?
Submit this response on Gradescope.
What kind of error is this?
> -- I expect this expression to evaluate to 4
> div 4 2-1
1
Choose:
- Syntax error
- Type Error
- Logic error (program works, but not as intended)
- Some other kind of error
After you answer: what happened?
Haskell successfully parsed, type-checked, and evaluated the expression. The problem is that it grouped the expression differently from what the programmer intended:
(div 4 2) - 1
which gives 1.
To divide 4 by the result of 2 - 1, write:
div 4 (2 - 1)
This is a logic error. Nothing prevents the program from running; it just does the wrong thing.
Warmup 2¶
Gradescope question: What kind of error?
Submit this response on Gradescope.
What kind of error is this?
> 2 + -1
<interactive>:7:1: error:
Precedence parsing error
cannot mix ‘+’ [infixl 6] and prefix `-' [infixl 6]
in the same infix expression
Choose:
- Syntax error
- Type Error
- Logic error (program works, but not as intended)
- Some other kind of error
After you answer: what happened?
Haskell cannot parse the expression in the intended way. Write the negative number as a subexpression:
2 + (-1)
This is a syntax/parse error.
Warmup 3¶
Gradescope question: What kind of error?
Submit this response on Gradescope.
What kind of error is this if these definitions occur in the same scope?
x = 42
x = 100
Choose:
- Syntax error
- Type Error
- Logic error (program works, but not as intended)
- Some other kind of error
After you answer: what happened?
Each definition is perfectly reasonable by itself. The problem is the rule from Module 02.1 that a name can be defined only once in a given scope.
This is therefore some other kind of error: a multiple-definition error.
Warmup 4¶
Gradescope question: What kind of error?
Submit this response on Gradescope.
What kind of error is this?
> 'a' && True
<interactive>:10:1: error:
• Couldn't match expected type ‘Bool’ with actual type ‘Char’
• In the first argument of ‘(&&)’, namely ‘'a'’
In the expression: 'a' && True
In an equation for ‘it’: it = 'a' && True
Choose:
- Syntax error
- Type Error
- Logic error (program works, but not as intended)
- Some other kind of error
After you answer: what happened?
&& expects Boolean operands. 'a' has type Char, not Bool.
This is a type error.
Try it in ghci — nothing to submit
Create one example of each of the following:
- an expression that produces a parse error,
- an expression that produces a type error, and
- a valid expression that runs but gives a result you did not intend.
The third one is usually the hardest because Haskell cannot tell you that your intention was different from what you wrote.
Pure Expressions and Referential Transparency¶
Module 02.1 ended with a first description of functional programming:
In functional programming, computation proceeds by evaluating expressions.
Now we need to make that statement more precise.
Some vocabulary¶
An expression is a piece of computation:
1 + 2
sqrt 4
f 3
To evaluate an expression is to try to compute its result.
Evaluation has two broad possibilities:
- it terminates and produces a result, or
- it diverges, meaning evaluation continues forever.
For example:
1 + 2
terminates with 3.
A recursive computation with no base case might diverge.
A side effect is an observable change caused while an expression is being evaluated. Familiar examples from imperative programming include:
- changing mutable state,
- printing to the screen,
- writing a file,
- sending something over a network.
A pure expression does not perform side effects as part of its evaluation.
That gives us an especially useful property called referential transparency:
An expression is referentially transparent if replacing it with its result does not change the meaning of the surrounding expression.
Suppose:
x = 1 + 2
Then in a pure setting we can reason about:
x * x
by replacing x with what it means:
(1 + 2) * (1 + 2)
and then, once 1 + 2 evaluates to 3, by reasoning about:
3 * 3
Nothing surprising happened while the subexpression was evaluated. It did not secretly increment a counter, alter some shared object, or print something to the screen.
That makes local reasoning much easier.
Why side effects complicate substitution¶
Imagine an imperative expression whose evaluation both produces a value and changes a variable. Replacing that expression with the value it produced would remove the state change. The replacement would therefore change the behavior of the surrounding program.
That is exactly what referential transparency rules out.
Haskell programs can still do I/O
Real Haskell programs can print, read files, communicate over networks, and interact with the outside world. Haskell does not make useful programs impossible.
The important distinction is that Haskell does not treat those effects as invisible side effects of ordinary pure expressions. It represents and sequences effectful computations explicitly. We will learn the machinery for that later.
For now, the key consequences are:
- ordinary Haskell values do not mutate,
- pure expressions can be reasoned about by substitution,
- the timing of an expression's evaluation is less constrained by hidden state.
That third consequence leads directly to lazy evaluation.
Lazy Evaluation: Compute Only When Needed¶
Most languages you have used probably evaluate eagerly, also called strictly.
Under eager evaluation, subexpressions are ordinarily evaluated before the larger expression that uses them.
For example, imagine:
x = 1 + 2
Under an eager model:
x = 1 + 2
-----
3
x = 3
Likewise, an eager language encountering:
f(1 + 2, 3 * 4)
would ordinarily evaluate the arguments first:
f(3, 12)
and then enter f.
Haskell instead uses lazy evaluation: an expression is evaluated when its value is actually needed.
So:
x = 1 + 2
does not require Haskell to immediately compute 3. Conceptually, x can remain bound to the unevaluated expression 1 + 2 until something demands its result.
A dramatic example: undefined¶
Haskell provides a special value named undefined.
You can think of it as a placeholder that says: "If you ever actually need this value, crash."
Try this:
x = undefined
Nothing has to go wrong yet. Haskell can create the binding without evaluating undefined.
But if you then ask ghci to evaluate and print:
x
Haskell now needs the result, so it evaluates undefined and fails.
Try it in ghci — nothing to submit
Try:
let x = undefined in 10
and then:
let x = undefined in x
Before running them, predict which expression needs the value of x.
The first example captures the essence of laziness: an expression can exist without ever being evaluated.
Lazy function arguments¶
Consider:
ignoreFirstArgument :: Integer -> Integer -> Integer
ignoreFirstArgument x y = y
Now call:
ignoreFirstArgument (1 + 2) (3 * 4)
Under eager evaluation, both arguments would be evaluated first.
Under lazy evaluation, Haskell enters the function body without first evaluating either argument. The body is simply:
y
so the first argument is never needed at all. The call can reduce conceptually to:
3 * 4
and eventually to:
12
The 1 + 2 computation never happens.
If a parameter will not be used, Haskell lets us say that explicitly with a wildcard pattern:
ignoreFirstArgument :: Integer -> Integer -> Integer
ignoreFirstArgument _ y = y
The _ means roughly:
"Something must be supplied here, but I am not giving it a name because I will not use it."
Now the undefined experiment becomes even more revealing:
ignoreFirstArgument undefined 5
returns 5.
But:
ignoreFirstArgument 5 undefined
eventually needs the second argument and fails.
Lazy evaluation is usually call-by-need¶
Haskell does something more useful than merely postponing computation. Once a delayed expression has been evaluated, its result can ordinarily be shared rather than recomputed every time the same binding is used.
Imagine a deliberately slow computation:
y = expensive 3 7
Creating the binding can be immediate. The first time y is demanded, Haskell performs the expensive computation. Later uses of that same evaluated binding can reuse the result.
This combination of delay and sharing is often called call-by-need.
It is important to distinguish sharing a binding from automatically noticing repeated text. These are different:
expensive 3 7 + expensive 3 7
contains two separate occurrences of the computation.
But:
let x = expensive 3 7
in x + x
gives both uses of x access to the same delayed computation.
Why laziness can be useful¶
Lazy evaluation can provide several benefits:
- No unnecessary computation. If a value is never needed, Haskell need not compute it.
- Sharing can avoid repeated work. A delayed binding can be evaluated once and reused.
- Large data structures can be consumed incrementally. We can compute the portion that is demanded rather than constructing everything first.
- Infinite structures become useful. An infinite object is not a problem if the program only asks for a finite part of it.
- Pure computations have flexible evaluation order. Because independent pure expressions cannot interfere through hidden state, a compiler or runtime has more freedom about when to evaluate them. This also makes some forms of parallel evaluation easier, although laziness does not automatically make a program parallel.
Laziness also has costs¶
Before reading the list below, answer the Gradescope question.
Gradescope question: Downsides of lazy evaluation
Submit this response on Gradescope.
Can you think of some downsides of lazy evaluation?
There is no single intended answer. Some common tradeoffs include:
- Performance can become harder to predict. The place where an expression is written is not necessarily the place where its cost is paid.
- Memory use can become surprising. A program may accumulate many delayed computations, sometimes producing what Haskell programmers call a space leak.
- Errors can appear later than expected. A bad expression may sit unevaluated until some distant part of the program finally demands it.
- Reasoning about time and resource use is harder than reasoning about pure results. Referential transparency helps with what a computation means, but laziness can complicate when work happens.
Lazy evaluation is therefore not "free efficiency." It changes the evaluation strategy and creates a different set of opportunities and problems.
Laziness, Termination, and Divergence¶
The most striking consequence of lazy evaluation is that a program can contain a diverging computation and still terminate, if it never needs that computation.
Suppose:
addForever 10
diverges. It keeps computing forever and never produces a result.
Now consider a let.
Gradescope question: Will this terminate?
Submit this response on Gradescope.
Under lazy evaluation, will this expression terminate?
Assume that evaluating addForever 10 diverges on all inputs.
let x = addForever 10
in x
Choose:
- Yes, the expression will terminate.
- No, the expression will diverge.
After you answer: trace what is demanded
Creating the binding does not force addForever 10.
But the result of the whole let expression is its body, and the body is x. To produce that result, Haskell must evaluate x, which means evaluating addForever 10.
The expression therefore diverges.
Gradescope question: Will this terminate?
Submit this response on Gradescope.
Under lazy evaluation, will this expression terminate?
Assume that evaluating addForever 10 diverges on all inputs.
let x = addForever 10
y = 10
in x
Choose:
- Yes, the expression will terminate.
- No, the expression will diverge.
After you answer: trace what is demanded
As currently written, the body is still x, so Haskell still demands addForever 10. The extra binding y = 10 does not change that.
The expression therefore diverges.
The more revealing contrast is this nearby expression:
let x = addForever 10
y = 10
in y
Here the result is y, so Haskell never needs x. The diverging computation can remain unevaluated forever while the whole expression terminates immediately with 10.
Pause and predict — nothing to submit
Consider:
ignoreFirstArgument (addForever 10) 42
Assuming addForever 10 diverges, does the whole expression terminate?
Do not focus on where the diverging computation is written. Ask only: Will the result ever be demanded?
Operators: How Does Haskell Know What You Mean?¶
Consider:
5 - 1 - 2 * 2
Without any grouping, several interpretations seem possible:
(5 - 1) - (2 * 2) -- 0
((5 - 1) - 2) * 2 -- 4
(5 - (1 - 2)) * 2 -- 12
Humans who have spent years doing arithmetic barely notice the ambiguity. We learned an order of operations long ago.
A programming language needs explicit rules too.
Operators and operands¶
In:
1 + 2
+ is an operator, and 1 and 2 are its operands.
The expression contains an application of the + operator.
When a string of operators could be grouped in multiple ways, the language must disambiguate it. Two properties do most of that work.
Precedence¶
Precedence determines how different operators group.
For example, multiplication has higher precedence than addition:
1 + 2 * 3
groups as:
1 + (2 * 3)
rather than:
(1 + 2) * 3
Associativity¶
Associativity determines how repeated operators at the same precedence level group.
Subtraction is left-associative:
1 - 2 - 3
groups as:
(1 - 2) - 3
Exponentiation is right-associative:
2 ^ 3 ^ 2
groups as:
2 ^ (3 ^ 2)
Associativity is about grouping, not evaluation order
This distinction matters especially in a language with lazy evaluation.
Saying that - is left-associative means Haskell parses a - b - c as (a - b) - c. It does not mean that Haskell must eagerly evaluate the left side first at runtime.
A useful Haskell precedence table¶
You do not need to memorize every precedence level. What matters is being able to reason about the common ones and to add parentheses when you want to be explicit.
| Operation | Syntax | Fixity / grouping |
|---|---|---|
| function application | f x |
highest; left-associative |
| exponentiation | ^, ** |
precedence 8; right-associative |
| multiplication / division | *, /, div, mod |
precedence 7; left-associative |
| addition / subtraction | +, - |
precedence 6; left-associative |
| list concatenation | ++ |
precedence 5; right-associative |
| comparisons | ==, /=, <, >, <=, >= |
precedence 4; non-associative |
| logical and | && |
precedence 3; right-associative |
| logical or | || |
precedence 2; right-associative |
The most important entry for reading ordinary Haskell code is the first:
Function application binds more tightly than infix operators.
So:
show 4 ++ show 2
groups as:
(show 4) ++ (show 2)
And:
f 12 - 2
groups as:
(f 12) - 2
Put the rules together¶
Now use both facts:
- function application has higher precedence than subtraction;
- subtraction is left-associative.
Gradescope question: Operators
Submit this response on Gradescope.
In Haskell, which of the following expressions is equivalent to:
f 12 - 2 - 3
Choose:
((f 12) - 2) - 3(f (12 - 2)) - 3(f 12) - (2 - 3)
After you answer: group it in two stages
First, function application binds tightly:
(f 12) - 2 - 3
Then subtraction associates to the left:
((f 12) - 2) - 3
Try it in ghci — nothing to submit
Define:
f n = n * 10
Then compare:
f 12 - 2 - 3
((f 12) - 2) - 3
(f (12 - 2)) - 3
(f 12) - (2 - 3)
The numerical results make the different groupings visible.
Operators Are Functions¶
Haskell now gives us a useful surprise:
A binary operator is just a function written in infix notation.
We usually write:
1 + 2
But we can refer to the + function directly by putting the symbolic name in parentheses:
(+) 1 2
These mean the same thing.
Likewise:
(&&) True False
uses the && operator as an ordinary function.
Ask ghci for its type:
> :t (&&)
(&&) :: Bool -> Bool -> Bool
That type should now look familiar: && is a function involving two Bool inputs and a Bool result. Soon we will become more precise about what those two arrows mean.
Functions can be written as operators too¶
The relationship works in the other direction.
A two-argument function with an ordinary name can be written between its arguments by surrounding its name with backticks:
div 4 2
4 `div` 2
and:
mod 17 12
17 `mod` 12
Both pairs are equivalent.
Try it in ghci — nothing to submit
Compare:
(+) 4 5
4 + 5
div 17 5
17 `div` 5
mod 17 12
17 `mod` 12
Then ask:
:t (+)
:t div
:t mod
The surface notation changes. The underlying idea does not: these are functions.
A brief advanced feature: fixity declarations¶
Suppose we define:
addMod60 :: Integer -> Integer -> Integer
addMod60 x y = (x + y) `mod` 60
We can use it infix:
50 `addMod60` 15
which gives 5.
But once a function is used infix, Haskell needs to know its fixity: its precedence and associativity.
An infix function without an explicit declaration receives Haskell's default fixity, which is left-associative at precedence 9. That is higher than multiplication's precedence 7.
So without a declaration:
0 * 1 `addMod60` 2
groups like:
0 * (1 `addMod60` 2)
and evaluates to 0.
If we want addMod60 to group at the same precedence as mod, we can declare:
infixl 7 `addMod60`
Now:
0 * 1 `addMod60` 2
groups left-to-right at precedence 7:
(0 * 1) `addMod60` 2
and evaluates to 2.
You will not need to invent many custom operators in this course. The useful conceptual lesson is that operator syntax is part of a language's design, and Haskell exposes that design machinery to programmers.
The Big Reveal: Every Haskell Function Takes One Argument¶
Now for a claim that initially sounds false:
Every Haskell function takes exactly one argument.
But we just wrote things like:
average :: Double -> Double -> Double
average x y = (x + y) / 2
and called:
average 1.0 2.0
That certainly looks like one function call with two arguments.
Before explaining what is happening, consider a strange GHCi experiment.
A strange partial call¶
Suppose average is loaded.
If we type:
> average 1.0
ghci produces an error about being unable to Show or print a function of type:
Double -> Double
But if we instead bind the expression:
> temp = average 1.0
there is no corresponding error.
And then:
> :t temp
temp :: Double -> Double
works.
Gradescope question: Currying mystery
Submit this response on Gradescope before reading the explanation below.
What do you think is going on here?
Why do we get an error when ghci tries to display:
average 1.0
but not when we bind the same expression:
temp = average 1.0
The key is that average 1.0 is not an invalid or incomplete expression.
It successfully evaluates to a value.
That value is a function.
The error appears only because the REPL follows its usual Read-Eval-Print-Loop behavior and tries to print the result. Haskell does not have a general way to display an arbitrary function as text.
When we write:
temp = average 1.0
we give that function value a name instead of asking ghci to print it.
Then:
temp 2.0
produces:
1.5
This is our first concrete example of the title of this module:
Functions are values.
Currying: One Argument at a Time¶
To understand exactly what happened, look at the type again:
average :: Double -> Double -> Double
In Module 02.1, we read this informally as:
"
averagetakes twoDoubles and results in aDouble."
That remains a useful reading.
But -> itself groups to the right.
So the type is really:
average :: Double -> (Double -> Double)
Now read it literally:
"
averagetakes oneDoubleand results in a function of typeDouble -> Double."
That result function takes the second Double.
Function application groups the opposite way¶
There is a satisfying symmetry here.
Function application groups to the left:
average 1.0 2.0
means:
(average 1.0) 2.0
Function types group to the right:
Double -> Double -> Double
means:
Double -> (Double -> Double)
Put those facts together:
average
|
| apply to 1.0
v
a function :: Double -> Double
|
| apply to 2.0
v
1.5
What looked like one two-argument call is really a sequence of one-argument calls.
This representation of multi-argument functions is called currying.
A curried function is a function that accepts its arguments one at a time. Haskell's multi-argument function syntax is curried.
Supplying fewer arguments than a function can eventually accept is called partial application:
average 1.0
partially applies average and produces a new function.
Try it in ghci — nothing to submit
With average loaded, try:
:t average
:t average 1.0
Then:
averageWith10 = average 10.0
Ask for its type:
:t averageWith10
Finally try:
averageWith10 20.0
averageWith10 4.0
You have created a specialized function by supplying only the first argument.

Reading a longer curried type¶
Now consider:
f :: Bool -> Bool -> Bool -> Bool
Because -> associates to the right, this means:
f :: Bool -> (Bool -> (Bool -> Bool))
There are therefore several equivalent levels at which we can describe f.
Gradescope question: Reading a curried function type
Submit this response on Gradescope. Select all that apply.
How would you describe the type of this function?
f :: Bool -> Bool -> Bool -> Bool
fis a function that takes fourBools.fis a function that takes a function fromBooltoBooland results in a function fromBooltoBool.fis a function that takes aBooland results in a function that takes twoBools and results in aBool.fis a function that takes threeBools and results in aBool.
After you answer: two descriptions are compatible
The third and fourth descriptions are both useful and correct.
At a convenient high level, we can say:
ftakes threeBools and results in aBool.
At the curried level, we can expose the first application:
ftakes oneBooland results in a function that takes two moreBools and results in aBool.
These are two descriptions of the same function type.
A small history aside¶
The name currying comes from logician Haskell Curry, who also gave his name to the Haskell programming language.
Curry was not the first person to describe the idea. Moses Schönfinkel described essentially the same technique earlier, and the historical story extends further back still. Technical names often preserve a complicated history of discovery and rediscovery.
Higher-Order Functions and First-Class Functions¶
Once average 1.0 can itself be a value, functions can participate in ordinary programming in a new way.
A higher-order function is a function that:
- takes a function as an input, or
- produces a function as an output.
Currying already gives us examples of functions whose results are functions:
average :: Double -> (Double -> Double)
After the first argument, average produces another function.
Haskell also lets us pass functions into other functions, which we will use constantly.
A language where functions can be stored, passed, and returned as ordinary values is said to have first-class functions.
This reconnects to a major course theme:
Programs = Data.
A function describes computation, but in Haskell a function can also be a value that another computation receives, stores, or returns.
That is a powerful form of abstraction.
Partial Application in Practice¶
Currying lets us make new functions by supplying only some of a function's inputs.
We already did this with:
averageWith10 = average 10.0
The same idea works with operators.
Recall that the symbolic operator + can be used as an ordinary prefix function:
(+) :: Integer -> Integer -> Integer
If we supply only its first input:
(+) 1
the result is a function of type:
Integer -> Integer
That function waits for one more integer and then adds it to 1.
So these definitions behave the same:
increment1 :: Integer -> Integer
increment1 n = 1 + n
increment2 :: Integer -> Integer
increment2 = (+) 1
The second definition contains no explicit parameter. This is point-free style: defining a function in terms of other functions without directly naming the value it will eventually receive.
Operator sections: an especially convenient shorthand¶
Haskell also has operator sections, which let us fix either side of an infix operator.
For example:
(+ 1)
means "a function that takes a value and adds 1 on the right":
(+ 1) 5 ==> 5 + 1 ==> 6
Similarly:
(* 2)
means "multiply the input by 2," and:
(^ 2)
means "raise the input to the second power."
That gives us compact definitions:
increment :: Integer -> Integer
increment = (+ 1)
double :: Integer -> Integer
double = (* 2)
square :: Integer -> Integer
square = (^ 2)
Notice that an operator section such as (^ 2) is not the same syntactic construction as prefix partial application such as (^) 2: the first fixes the right operand, while the second fixes the left operand. For commutative operators such as + and *, the distinction can be easy to miss. For exponentiation it matters a lot.
Point-free style and operator sections are useful when they make a definition clearer. They are not a contest to eliminate every variable name.
You can use a section with strings too:
cowSay :: String -> String
cowSay = ("The cow says: " ++)
Then:
cowSay "moo"
produces:
"The cow says: moo"
Try it in ghci — nothing to submit
Define these two ways and confirm that they behave the same:
double1 n = n * 2
double2 = (* 2)
Then compare these two functions:
powerOfTwo = (^) 2
square = (^ 2)
Try each on 3. The difference makes the left-section/right-section distinction visible.
Function Composition: Build Big Functions from Small Ones¶
We now have the pieces needed for one of the most characteristic functional-programming moves.
Suppose:
increment :: Integer -> Integer
increment = (+ 1)
double :: Integer -> Integer
double = (* 2)
square :: Integer -> Integer
square = (^ 2)
Now look at:
increment (double 1)
double (increment 1)
increment (square 1)
Each expression has the same general shape:
apply one function, then feed its result into another function.
That operation is function composition.
Deriving composition by abstraction¶
Start with one fixed input, 1.
We want an abstraction over:
increment (double 1)
The two things that can vary are the functions.
Call the outer function g and the inner function f:
compose1 :: (Integer -> Integer)
-> (Integer -> Integer)
-> Integer
compose1 g f = g (f 1)
Examples:
compose1 increment double
compose1 double increment
compose1 increment square
But there is no reason to hard-code the input 1.
Parameterize that too:
compose :: (Integer -> Integer)
-> (Integer -> Integer)
-> Integer
-> Integer
compose g f n = g (f n)
Now:
compose increment double 10
means:
increment (double 10)
The important feature is that compose takes functions as arguments. It is a higher-order function.
Haskell already has composition¶
Composition is so useful that Haskell provides it as the operator (.).
The general type is:
(.) :: (b -> c) -> (a -> b) -> a -> c
The lowercase type names a, b, and c mean that composition is not restricted to Integer. The only requirement is that the types line up:
a --f--> b --g--> c
Then:
g . f
is a new function:
a ---------> c
defined by:
(g . f) x = g (f x)
The function on the right runs first.
So:
(increment . double) 1
means:
increment (double 1)
and gives 3.
But:
(double . increment) 1
means:
double (increment 1)
and gives 4.
Order matters.
Try it in ghci — nothing to submit
Define:
increment = (+ 1)
double = (* 2)
square = (^ 2)
Then predict and evaluate:
(increment . double) 5
(double . increment) 5
(square . increment) 5
(increment . square) 5
Finally create a named composed function:
doubleThenIncrement = increment . double
and try it on several inputs.
Why composition matters¶
The point is larger than saving parentheses.
Functional programming encourages us to build small pieces that do one thing, then combine them using general operations.
With:
- functions as values,
- higher-order functions,
- partial application, and
- composition,
we can create new computations by assembling existing ones.
This is the same abstraction move from Module 02.1 at a higher level.
In Module 02.1, we abstracted over repeated expressions.
Here, we are abstracting over and combining functions themselves.

How the Pieces Fit Together¶
The topics in this module are tightly connected.
Purity makes evaluation flexible¶
Because pure expressions do not secretly modify state, replacing a pure expression with its result does not change the surrounding computation.
That is referential transparency.
Referential transparency supports laziness¶
If evaluating an expression now or later cannot change the meaning of unrelated computation, Haskell can postpone work until the result is demanded.
Function application is an operation with grouping rules¶
Once we pay attention to precedence and associativity, Haskell's lightweight function syntax becomes less mysterious:
f x y
is left-associated function application.
Currying turns "many arguments" into functions returning functions¶
f :: A -> B -> C
means:
f :: A -> (B -> C)
So partially applying a function naturally produces another function.
Functions can therefore be values¶
We can bind them:
temp = average 1.0
pass them to other functions, return them from functions, and combine them.
Composition gives us an abstraction for combining computations¶
g . f
creates a new function from two existing functions.
The title Functions = Values is therefore not just a slogan. It is the idea that makes much of Haskell's functional style possible.
Where This Leaves Us¶
You now have a more complete functional-programming mental model:
- Pure expressions are referentially transparent. We can reason about them without hidden state changes.
- Haskell evaluates lazily. Work can be delayed until its result is actually demanded.
- Precedence and associativity determine grouping. Function application binds especially tightly.
- Operators are functions. Haskell lets us move between ordinary function notation and infix notation.
- Multi-argument functions are curried. What looks like several arguments is a chain of one-argument applications.
- Functions are values. They can be bound, passed, and returned.
- Higher-order functions operate on functions.
- Partial application creates specialized functions.
- Function composition combines small computations into larger ones.
The next step is to apply these ideas in code rather than just recognize them. In particular, HW2 will ask you to work under unusual constraints that make function abstraction, higher-order behavior, and repeated application impossible to ignore.
Finish the Module 2.2 Completion¶
At this point, you should have encountered every substantive question in the Module 2.2 Completion assignment on Gradescope.
Before submitting, Gradescope also asks you for two pieces of feedback that are not content questions:
Gradescope: Time spent
Submit this response on Gradescope.
Approximately how much time did you spend on this module?
Gradescope: Remaining questions and thoughts
Submit this response on Gradescope.
What lingering questions or thoughts do you have about this module?
That is the end of Module 02.2.
Reference Slides¶
Slides for this module (Google Slides)
About the slides
When possible, a corresponding slide deck will be linked for a module. These slides are for reference only: they come from a previous iteration of the course, may be out of date, and will not always be provided. This module page is the ultimate source of truth for what you are responsible for.