liveBook · Manning

Referential transparency

As explained in Functional Programming in Scala, Second Edition

Referential transparency is the property that an expression can be replaced by its value without changing a program's meaning within a specified context.

Referential transparency is the property that an expression may be replaced by the value it evaluates to without changing a program’s meaning. A pure function has this property for every referentially transparent input: it computes a result determined only by its input and has no observable effect besides returning that result. Addition and the length of an immutable string fit this model. section 1.2

The practical test is substitution. If an expression stands for a value, replace each occurrence with that value and ask whether the surrounding program behaves the same. When replacement is safe, computation can be understood like algebra: expand terms, substitute equals for equals, and reduce the expression. This supports local reasoning, since a transparent expression keeps its meaning wherever it appears. section 1.3 section 4.1

A hidden charge fails substitution

A coffee-purchase function may return only a Coffee while also charging a credit card. Replacing buyCoffee(aliceCreditCard) with Coffee() preserves the returned cup but removes the authorization request. The two expressions are observably different, so the function is not referentially transparent. The invariant is demanding but useful: everything a function does must be represented by its returned value and result type. section 1.3

scala> val x = new StringBuilder("Hello")
x: java.lang.StringBuilder = Hello
 
scala> val r1 = x.append(", World").toString
r1: java.lang.String = Hello, World
 
scala> val r2 = x.append(", World").toString
r2: java.lang.String = Hello, World, World      #1

Repeated mutation makes an apparently similar expression yield different results: the first append produces “Hello, World”; the second observes the already-modified builder and produces “Hello, World, World”. Replacing either call with its earlier result would change behavior. section 1.3

Not every use of mutation makes a whole function impure. Mutation confined to data created inside a function can remain unobservable to callers. An in-place quicksort may mutate a local array, then return a list; because no outside code can retain the array reference, quicksort(xs) can still be substituted by its resulting list for transparent xs. Purity concerns the externally observable expression, not a ban on every operational technique. section 14.1

When substitution fails, make the relevant effect explicit rather than hiding it:

  • Return a generated value together with the next state. A random-number generator can return (Int, RNG), leaving the old generator unchanged and giving the caller control over subsequent state. section 6.2
  • Return failure as an ordinary value such as Option, Either, Try, or Validated instead of throwing. This keeps error possibilities available to types and composable operations. section 4.0 section 4.6
  • Represent I/O requests as a pure program and choose an interpreter separately. The interpreter may use real effects or a pure State or Reader interpretation for testing. section 13.4.3 section 13.10

Transparency is contextual. Constructing two equal-looking objects can create distinct references, which most code cannot observe but reference equality can. Thus an expression is referentially transparent with respect to a program when replacing its occurrences does not affect that program’s meaning. The question is not merely whether an implementation performs an action, but whether the surrounding context can observe a difference. section 14.3

How Functional Programming in Scala, Second Edition covers it

How deep it goes
Defines substitution precisely, contrasts effects and mutation, and later refines transparency as dependent on observable context.
Examples
Uses coffee charging, StringBuilder mutation, thrown exceptions, random-number generators, local quicksort mutation, and reference equality.
What it assumes
Introduces purity and substitution from concrete Scala expressions without assuming functional-programming terminology.
What you can do after
Evaluate/understand whether substitutions preserve meaning and redesign hidden effects as explicit returned values or programs.
How it connects
Connects pure functions, side effects, the substitution model, exceptions, explicit state, error ADTs, and I/O interpreters.

Also explained in

Compare how 9 books explain referential transparency →

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.