A pure function computes a result from its inputs and has no observable effect beyond returning that result. For a given input, its result is determined solely by that input; changing internal or external state is irrelevant. Mutating variables or objects, throwing exceptions, printing, reading input, and accessing files or networks are side effects when they are observable from the call. section 1.0 section 1.2
Purity is tested through referential transparency: an expression is referentially transparent when replacing it with its evaluated result does not change a program’s meaning. This supports the substitution model—understand a call by inspecting its definition and substituting its arguments, rather than tracing a history of state updates. The result is local reasoning, modular components, and safer reuse, testing, composition, refactoring, and parallelization. section 1.3 section 1.5
Mutation that breaks substitution
Calling StringBuilder.append changes the same builder in place. Two calls that appear identical can therefore yield different strings because the first call has changed the object used by the second. Replacing a shared expression with repeated append calls changes the outcome, showing that append is not pure. section 1.3
Purity does not require every implementation step to avoid mutation. A function may allocate and mutate state locally, then return a result without exposing that state. In-place quicksort can mutate a locally created array and still present a referentially transparent List[Int] to List[Int] interface, because callers cannot observe the array’s intermediate changes. section 14.1
When an operation performs effects, preserve a pure core by separating responsibilities:
- Compute the domain result in a pure function, such as selecting a winning Player or returning no winner for a draw. section 13.1
- Convert that result into a pure description, such as a display message. section 13.1
- Keep printing or other I/O at the outermost layer, where it interprets the computed result. section 13.1
def winnerMsg(p: Option[Player]): String = p
.map: #1
case Player(name, _) => s"$name is the winner!"
.getOrElse("It's a draw.")
def contest(p1: Player, p2: Player): Unit = #2
println(winnerMsg(winner(p1, p2)))
The contest refactoring makes the boundary visible: winner and winnerMsg compute values, while the final println performs the effect. section 13.1
The same boundary can model stateful APIs: accept the current state explicitly and return both a result and new state. More generally, an impure operation can be split into a pure function that produces a description and an effectful interpreter that acts on that description. This keeps programs composable even when they ultimately perform I/O. section 6.7 section 13.1


















