Reading — step 1 of 7
Learn
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:
- Returns the same output for the same input (deterministic)
- 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…