liveBook · Manning

Pure function

As explained in Functional Programming in Scala, Second Edition

A pure function is a function whose result depends only on its arguments and whose evaluation has no observable side effects.

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

How Functional Programming in Scala, Second Edition covers it

How deep it goes
Defines purity through referential transparency, demonstrates substitution failure, and qualifies the rule with locally scoped mutation.
Examples
Contrasts StringBuilder.append with string reversal, then refactors a player contest and implements local-mutation quicksort.
What it assumes
Introduces pure-function programming alongside Scala basics for readers new to Scala or functional programming.
What you can do after
Apply it by extracting pure computations from effectful procedures and isolating I/O at program boundaries.
How it connects
Connects referential transparency, the substitution model, local reasoning, state, I/O, and composability.

Also explained in

Compare how 19 books explain pure function →

Related concepts

This explainer was generated by Manning's knowledge graph from the text of Functional Programming in Scala, Second Edition. The section links point to the parts of the book it draws on.