Immutability in Practice
Prerequisites: Case classes, Collection operations
Scala code usually models change as new values rather than modified ones. This lesson shows the patterns that make that practical.
State as a value
case class Cart(items: Map[String, Int] = Map.empty):
def add(item: String, qty: Int = 1): Cart =
copy(items = items.updated(item, items.getOrElse(item, 0) + qty))
def remove(item: String): Cart = copy(items = items - item)
def count: Int = items.values.sum
@main def carts(): Unit =
val empty = Cart()
val cart = empty.add("apple", 3).add("pear").add("apple").remove("pear")
println(cart)
println(cart.count)
println(empty.count)
Cart(Map(apple -> 4))
4
0
Every operation returns a new Cart; empty is still empty. You get
undo, history, and thread safety for free, because no value is ever
modified.
Folding over events
A common pattern: the current state is the result of applying events to an initial state, one after another:
enum Event:
case Deposited(amount: BigDecimal)
case Withdrew(amount: BigDecimal)
case class Account(balance: BigDecimal, transactions: Int)
def apply(account: Account, event: Event): Account = event match
case Event.Deposited(a) => Account(account.balance + a, account.transactions + 1)
case Event.Withdrew(a) => Account(account.balance - a, account.transactions + 1)
@main def events(): Unit =
val history = List(Event.Deposited(100), Event.Withdrew(30), Event.Deposited(15))
println(history.foldLeft(Account(0, 0))(apply))
println(history.scanLeft(BigDecimal(0))((b, e) => apply(Account(b, 0), e).balance))
Account(85,3)
List(0, 100, 70, 85)
scanLeft is like foldLeft, but keeps every intermediate result — the
balance after each event.
When mutation is fine
Local mutation that never escapes a method is harmless and sometimes clearer or faster:
def primesUpTo(n: Int): List[Int] =
val isComposite = Array.fill(n + 1)(false)
for
i <- 2 to n
if !isComposite(i)
j <- i * i to n by i
do isComposite(j) = true
(2 to n).filterNot(isComposite).toList
@main def sieve(): Unit =
println(primesUpTo(30))
List(2, 3, 5, 7, 11, 13, 17, 19, 23, 29)
From the outside, primesUpTo is a pure function: same input, same
output, no visible side effects.