Skip to content
Immutability and Pure Functions
step 1/7

Reading — step 1 of 7

Learn

~3 min readFunctional Patterns

Functional programming in Scala (FP in Scala by Chiusano & Bjarnason) treats immutability + pure functions as the bedrock. Idiomatic Scala leans heavily on these patterns even when not 'pure FP'.

Immutability — val over var

val x = 10           // immutable binding
x = 20               // ERROR — reassignment forbidden

var y = 10           // mutable
y = 20               // OK

Default to val. Use var only when you genuinely need to reassign the binding.

Immutable collections by default:

val list = List(1, 2, 3)            // immutable
val list2 = list :+ 4               // NEW list — list unchanged
list                                  // List(1, 2, 3)
list2                                 // List(1, 2, 3, 4)

import scala.collection.mutable
val mutList = mutable.ListBuffer(1, 2, 3)
mutList += 4                          // modifies in place

Scala has parallel mutable collections in scala.collection.mutable, but the default (when you write List, Map, Set) is immutable.

Why immutability

  • Concurrency: no data races without locks. Multiple threads can share immutable values.
  • Reasoning: a value never changes after creation. No need to track state mutations.
  • Equality: structural — two immutable values with the same contents are interchangeable.
  • Caching/sharing: safe to memoize, alias, hand to other functions.

The trade-off: copying. But Scala's immutable collections are structurally shared under the hood — list :+ 4 doesn't copy 1000 elements; it shares the structure and adds the new tail.

Pure functions

A pure function:

  1. Returns the same output for the same input (deterministic)
  2. Has no side effects (doesn't mutate state, do I/O, throw, etc.)
// Pure
def double(n: Int): Int = n * 2
def sum(list: List[Int]): Int = list.foldLeft(0)(_ + _)
def format(name: String, age: Int): String = s"$name is $age"

// Impure
def logAndDouble(n: Int): Int = {
    println(n)              // side effect: I/O
    n * 2
}
var counter = 0
def nextId(): Int = {
    counter += 1            // side effect: mutation
    counter
}

Pure functions are testable, parallelizable, composable. Aim for as many pure functions as possible; isolate impurity at the edges.

Substitution model

With pure functions, you can SUBSTITUTE a function call with its result without changing program behavior:

val x = double(5)            // 10
val y = double(5) + 1        // 11

// Equivalent (substitution):
val y2 = 10 + 1              // 11 — exact same result

This property powers refactoring confidence and equational reasoning. Lost as soon as functions have side effects.

Practical immutability — case classes and copy

case class User(name: String, age: Int, email: String)

val u = User("Ada", 36, "[email protected]")
val older = u.copy(age = 37)         // new User; u unchanged
val renamed = u.copy(name = "Linus")

The copy method (auto-generated by case class) creates a new instance with selected fields changed. "Modify" without mutating.

Common mistakes

  • Reaching for var by habit — most code can use val with re-assigning to a new val instead.
  • Using mutable collections without need — immutable is the default for a reason; only switch when profiling shows allocation overhead.
  • Mixing pure and impure carelessly — keep I/O, exceptions, mutation at the edges of your code; pure logic in the middle.
  • Side effects in maps/folds — these can run in any order, multiple times in lazy contexts. Don't mutate from inside.
  • Chaining many .copy calls — for deep updates, lenses (Cats / Monocle) are cleaner than nested copies.

Discussion

Ask a question, share an insight, or help someone who’s stuck.

Sign in to post a comment or reply.

Loading…