Make behavior a value
Higher-order and polymorphic functions: stop duplicating the loop, inject the policy.
You will build one reusable isSorted function that can check ascending numbers, descending numbers, or another type without changing its loop. The behavior changes because the caller supplies a function.
Retrieve before reading
If the same function checks both ascending and descending readings, what should change: the loop or the rule that compares two neighboring values?
Reveal a strong answer
Change the comparison function. The traversal stays the same; the caller injects the ordering policy.
Functions can be data
In Lesson 01, the inputs were readings and the output was a number. Scala also lets us pass a function as an input or return one as an output. A function that does either is a higher-order function.
def applyTwice[A](f: A => A, value: A): A =
f(f(value))
val addOne = (x: Int) => x + 1
applyTwice(addOne, 3) // 5
f is not a special command. It is an ordinary value with the type A => A. The function receives behavior from its caller, just as usageFor received readings from its caller.
Int => Int means “a function from Int to Int.” (A, A) => Boolean means “a function that receives two values of the same, still-unknown type and returns a decision.”
The reusable loop
Here is the important move: define the traversal once, then pass in the meaning of “in order.” In this version, ordered(a, b) returns true when a is allowed to appear before b.
def isSorted[A](as: Array[A], ordered: (A, A) => Boolean): Boolean = {
@annotation.tailrec
def go(n: Int): Boolean =
if (n >= as.length - 1) true
else if (!ordered(as(n), as(n + 1))) false
else go(n + 1)
go(0)
}
Notice what the loop does not know. It does not know whether the values are numbers, labels, timestamps, or ascending. It only asks the supplied function about each adjacent pair.
| Call | Injected policy | Meaning |
|---|---|---|
isSorted(xs, (a, b) => a <= b) | left is no larger | ascending |
isSorted(xs, (a, b) => a >= b) | left is no smaller | descending |
isSorted(labels, (a, b) => a.length <= b.length) | shorter comes first | generic policy |
Follow the types
Polymorphism replaces a concrete type with a type variable such as A. The implementation must work for every A, so the type signature removes irrelevant choices.
def partial1[A, B, C](a: A, f: (A, B) => C): B => C =
b => f(a, b)
Read the signature from the outside in:
- We already have an
Acalleda. - We have a function that needs an
Aand aBto produce aC. - We must return a function that needs a
Band produces aC.
There is only one useful way to obtain that C: wait for b, then call f(a, b). This is the book's “follow the types” skill: the signature acts like a constraint puzzle.
Composition is a pipeline
A second useful shape returns a function by joining two functions:
def compose[A, B, C](f: B => C, g: A => B): A => C =
a => f(g(a))
g runs first because its output has type B, exactly what f needs. For Flow Insight, that lets us describe a small pipeline without hard-coding every call site:
val liters: Reading => Double = reading => reading.liters
val roundOne: Double => Double = x => math.round(x * 10) / 10.0
val displayLiters: Reading => Double = compose(roundOne, liters)
This is the same design instinct as passing a Rust closure into a generic function: keep the traversal or pipeline stable, and inject the policy. Scala's arrow types make the contract visible in the function signature.
Interactive coding checkpoint
Change the values or the ordering function. The loop is identical in both runs; only the injected comparator changes.
Run isSorted
The comparator accepted every neighboring pair.
ordered(7.5, 9.0) → true ordered(9.0, 12.0) → true
Try this sequence:
- Run the default values with ascending order.
- Switch to descending. The same values should now fail.
- Enter
12, 10, 10, 4and run descending. Equal neighbors are allowed. - Enter
7, 11, 8and inspect the first failing pair.
Quick checks
What changes when the same isSorted loop checks descending values?
For compose(f, g), which function runs first?
Spaced review
Before the next lesson, answer these without looking back:
- What makes a function higher-order?
- Why can one
isSortedimplementation work for multiple types? - In
compose(f, g), why mustgrun beforef? - What did Lesson 01 teach you to keep at the effect boundary?
Keep the compact version nearby: Higher-order functions reference.
Primary source: Chiusano and Bjarnason, Functional Programming in Scala, Chapter 2. The official first-edition exercise repository provides the chapter exercise stubs and answers. This page is an original, self-contained lesson; the book PDF is not required to run the checkpoint.
Ask me a follow-up question whenever a type signature feels like a puzzle. That is exactly the skill we are practicing.