Mutable Collections — When Immutability Isn't Enough | Scala Programming Guide

- Published on

Introduction
Scala's philosophy emphasizes immutability, yet pragmatism demands we acknowledge that sometimes mutable collections make excellent sense: when building a collection iteratively from input, when implementing imperative algorithms naturally expressed with mutation, or when interfacing with legacy Java code that expects mutable collections. The key is using mutable collections internally in functions, converting to immutable at boundaries. This chapter explores when and why mutability is justified, what mutable collections Scala offers, and most importantly, how to use them safely without undermining your program's reasoning and testability. The golden rule is simple: contain mutation—use mutable collections for temporary, local work, then hand off immutable results to callers. When mutation is confined to a function's implementation, the function's contract remains pure: same input yields same output, no side effects visible to callers. Used this way, mutable collections become performance optimizations, not correctness problems.
Scala's philosophy emphasizes immutability, yet sometimes mutable collections make sense. This chapter explores when, why, and how to use mutable data structures—and how to do so safely within a largely immutable codebase.
When to Use Mutable Collections
Three legitimate reasons to reach for mutability. First, performance requirements: building a large collection iteratively is more efficient with a mutable buffer than repeated immutable concatenations. Consider appending 1,000,000 elements: using immutable Lists means creating a million new List nodes, each with two pointer allocations. Using an ArrayBuffer means amortized O(1) per append, with occasional geometric growth of the backing Array. The difference is orders of magnitude. Second, imperative algorithms: some algorithms (like graph traversal or dynamic programming) naturally express themselves with mutation. You can force these algorithms into purely functional style, but the result is often more complex and harder to follow than the imperative version. Third, interfacing with legacy code: Java libraries sometimes require mutable collections or modification-tracking. Mutability is essential when you're the one implementing that interface. Beyond these cases, immutability is preferred—it makes code easier to reason about, test, and parallelize. The golden rule encapsulates this wisdom:
Three legitimate reasons to reach for mutability:
Performance Requirements: Building a large collection iteratively is more efficient with a mutable buffer than repeated immutable concatenations.
Imperative Algorithms: Some algorithms (like graph traversal or dynamic programming) naturally express themselves with mutation.
Interfacing with Legacy Code: Java libraries sometimes require mutable collections or modification-tracking.
The Golden Rule: Use mutable collections internally, convert to immutable before returning from functions.
// Good pattern
def processData(input: List[String]): List[String] = {
val buffer = scala.collection.mutable.ListBuffer[String]()
for (item <- input) {
// Process and accumulate
buffer += item.toUpperCase
}
buffer.toList // Return immutable
}
// Bad pattern (mutable boundary)
def badProcessData(input: List[String]): scala.collection.mutable.ListBuffer[String] = {
val buffer = scala.collection.mutable.ListBuffer[String]()
for (item <- input) {
buffer += item.toUpperCase
}
buffer // Exposes mutable type - dangerous!
}
ArrayBuffer — Indexed Mutable Collection
ArrayBuffer combines mutable growth with indexed random access, making it the default choice for building indexed collections. ArrayBuffer is backed by an Array that grows geometrically (typically doubling) when it reaches capacity. This growth strategy ensures O(1) amortized time for appends. You can also insert elements at arbitrary positions, though this is O(n) for positions in the middle (because arrays must shift elements). Removal is similarly O(n) in the general case. The key use case for ArrayBuffer is accumulating elements in a loop where you don't know the final count upfront. The pattern is straightforward: create an empty ArrayBuffer, push elements via +=, then convert to an immutable collection before returning. This gives you efficient accumulation while maintaining a pure functional boundary.
import scala.collection.mutable
// Creation
val buffer = mutable.ArrayBuffer[String]()
// Add single element
buffer += "Inception"
buffer += "Interstellar"
// Add multiple elements
buffer ++= List("The Matrix", "Oppenheimer")
// Insert at position
buffer.insert(2, "Dune")
// buffer: ArrayBuffer(Inception, Interstellar, Dune, The Matrix, Oppenheimer)
// Access by index
val second = buffer(1)
// Result: "Interstellar"
// Update by index
buffer(0) = "2001: A Space Odyssey"
// buffer: ArrayBuffer(2001: A Space Odyssey, Interstellar, Dune, The Matrix, Oppenheimer)
// Remove by index
buffer.remove(2)
// buffer: ArrayBuffer(2001: A Space Odyssey, Interstellar, The Matrix, Oppenheimer)
// Remove by value
buffer -= "The Matrix"
// buffer: ArrayBuffer(2001: A Space Odyssey, Interstellar, Oppenheimer)
// Length and capacity
println(s"Size: ${buffer.length}")
// Output: "Size: 3"
// Clear all elements
buffer.clear()
// buffer: ArrayBuffer()
// Real-world: Building a student grade tracker
case class StudentGrade(student: String, assignment: String, score: Int)
def aggregateGrades(submissions: List[StudentGrade]): Map[String, List[Int]] = {
val grades = mutable.Map[String, mutable.ArrayBuffer[Int]]()
for (submission <- submissions) {
val key = submission.student
if (!grades.contains(key)) {
grades(key) = mutable.ArrayBuffer[Int]()
}
grades(key) += submission.score
}
// Convert to immutable
grades.map { case (student, scoreBuffer) =>
(student, scoreBuffer.toList)
}.toMap
}
val submissions = List(
StudentGrade("Alice", "Math", 95),
StudentGrade("Alice", "English", 88),
StudentGrade("Bob", "Math", 87),
StudentGrade("Bob", "English", 92)
)
val result = aggregateGrades(submissions)
// Result: Map(Alice -> List(95, 88), Bob -> List(87, 92))
ListBuffer — Linked List Alternative
ListBuffer provides efficient prepend and append, converting to List cheaply. ListBuffer combines the sequential operation efficiency of linked lists with the convenience of mutable modification. Unlike ArrayBuffer which is backed by an Array, ListBuffer maintains a linked structure, making prepend O(1) and append O(1) (amortized). When you call .toList on a ListBuffer, it's O(1) amortized because the underlying linked structure is reused directly. This makes ListBuffer ideal when you're building a list recursively or need both efficient prepend and append operations. The trade-off is slower random access (O(n) to access an arbitrary index), making ListBuffer unsuitable when you frequently access elements by position.
import scala.collection.mutable
// Creation
val buffer = mutable.ListBuffer[String]()
// Add elements
buffer += "Inception"
buffer += "Interstellar"
buffer += "The Matrix"
// Prepend with +=:
buffer +=: "Oppenheimer"
// buffer: ListBuffer(Oppenheimer, Inception, Interstellar, The Matrix)
// Efficient conversion to List
val finalList = buffer.toList
// Result: List(Oppenheimer, Inception, Interstellar, The Matrix)
// Real-world: Building a reverse-order movie queue
def buildReverseQueue(titles: List[String]): List[String] = {
val queue = mutable.ListBuffer[String]()
for (title <- titles) {
queue +=: title // Prepend for reverse
}
queue.toList
}
val movies = List("Movie 1", "Movie 2", "Movie 3")
val reversed = buildReverseQueue(movies)
// Result: List(Movie 3, Movie 2, Movie 1)
mutable.Map — Mutable Key-Value Store
Mutable Maps are useful when you're frequently updating values—adding new keys, updating existing values, or removing entries. Common use cases include caches, accumulators during computation, and state tracking in imperative algorithms. Mutable Maps provide direct assignment syntax (map(key) = value), making updates feel natural. The getOrElseUpdate method is particularly powerful: it returns the value if the key exists, otherwise computes the value, stores it, and returns it. This pattern is essential for implementing memoization caches efficiently. Like all mutable collections, restrict mutable Maps to local scope and convert to immutable before returning from functions.
import scala.collection.mutable
// Creation
val cache = mutable.Map[String, Double]()
// Add/update
cache("Inception") = 8.8
cache("Interstellar") = 8.6
cache += ("The Matrix" -> 8.7)
// Remove
cache -= "The Matrix"
// getOrElseUpdate - compute and cache
var computations = 0
def expensiveComputation(movie: String): Double = {
computations += 1
8.5 + (movie.length * 0.01)
}
val expensiveCache = mutable.Map[String, Double]()
expensiveCache.getOrElseUpdate("Inception", expensiveComputation("Inception"))
expensiveCache.getOrElseUpdate("Inception", expensiveComputation("Inception"))
// computations == 1 - second call used cache
// Update existing value
cache.update("Inception", 8.9)
// Iterate
for ((key, value) <- cache) {
println(s"$key: $value")
}
// Real-world: Request caching system
class RequestCache {
private val cache = mutable.Map[String, String]()
def get(key: String): Option[String] = cache.get(key)
def getOrFetch(key: String, fetcher: String => String): String = {
cache.getOrElseUpdate(key, fetcher(key))
}
def clear(): Unit = cache.clear()
}
val cache = new RequestCache()
cache.getOrFetch("user-1", fetchUser) // Fetches from source
cache.getOrFetch("user-1", fetchUser) // Uses cache
def fetchUser(id: String): String = s"User data for $id"
mutable.Set — Mutable Unique Collection
Mutable Sets track membership and enforce uniqueness, just like immutable Sets, but support in-place modification. Add elements with +=, remove with -=, and test membership with contains or the in operator. Mutable Sets are useful for tracking "seen" items (avoiding duplicate processing) or accumulating unique values. Like mutable Maps, restrict mutable Sets to local scope, particularly when implementing algorithms that need to track which elements have been processed. The pattern of maintaining a mutable Set of "seen" items while building an output collection is common when deduplicating data streams or implementing topological sorts.
import scala.collection.mutable
// Creation
val seen = mutable.Set[String]()
// Add
seen += "Inception"
seen += "Inception" // Duplicate ignored
seen += "Interstellar"
// seen: Set(Inception, Interstellar)
// Remove
seen -= "Interstellar"
// seen: Set(Inception)
// Contains
if (seen("The Matrix")) println("Seen before") else println("New movie")
// Output: "New movie"
// Add multiple
seen ++= List("The Matrix", "Oppenheimer", "Dune")
// Union, intersection, difference (same as immutable Set)
val userGenres = mutable.Set("sci-fi", "drama")
val recommendedGenres = mutable.Set("sci-fi", "action", "thriller")
val overlap = userGenres intersect recommendedGenres
// Result: Set(sci-fi)
// Real-world: Deduplicating log entries
def deduplicateEvents(events: List[String]): List[String] = {
val seen = mutable.Set[String]()
val unique = mutable.ListBuffer[String]()
for (event <- events) {
if (!seen(event)) {
seen += event
unique += event
}
}
unique.toList
}
val logs = List("error", "warning", "error", "info", "warning", "error", "info")
val deduplicated = deduplicateEvents(logs)
// Result: List(error, warning, info)
mutable.Queue and mutable.Stack
Queue and Stack are specialized mutable collections implementing FIFO (First In, First Out) and LIFO (Last In, First Out) semantics respectively. Queues are useful for task scheduling, breadth-first search, and event processing—anything where you want to process items in the order they arrive. Stacks are useful for depth-first search, expression evaluation, parsing, and call stacks—anything where you want to process items in reverse order of arrival. Both are straightforward: Queues support enqueue/dequeue operations, while Stacks support push/pop. Both provide peek operations (front for Queue, top for Stack) to inspect without removing. The key insight is choosing the right data structure for your algorithm: using a Queue where you need a Stack leads to subtle correctness bugs, so selecting the right structure upfront prevents mistakes.
import scala.collection.mutable
// Queue - First In, First Out
val queue = mutable.Queue[String]()
queue.enqueue("Task 1")
queue.enqueue("Task 2")
queue.enqueue("Task 3")
// queue: Queue(Task 1, Task 2, Task 3)
val nextTask = queue.dequeue()
// nextTask: "Task 1"
// queue: Queue(Task 2, Task 3)
// Peek without removing
val peek = queue.front
// Result: "Task 2"
// Stack - Last In, First Out
val stack = mutable.Stack[String]()
stack.push("Frame 1")
stack.push("Frame 2")
stack.push("Frame 3")
// stack: Stack(Frame 3, Frame 2, Frame 1)
val lastFrame = stack.pop()
// lastFrame: "Frame 3"
// stack: Stack(Frame 2, Frame 1)
// Peek without removing
val topFrame = stack.top
// Result: "Frame 2"
// Real-world: Expression evaluation (reverse Polish notation)
def evaluateRPN(tokens: List[String]): Double = {
val stack = mutable.Stack[Double]()
for (token <- tokens) {
token match {
case "+" =>
val b = stack.pop()
val a = stack.pop()
stack.push(a + b)
case "-" =>
val b = stack.pop()
val a = stack.pop()
stack.push(a - b)
case "*" =>
val b = stack.pop()
val a = stack.pop()
stack.push(a * b)
case "/" =>
val b = stack.pop()
val a = stack.pop()
stack.push(a / b)
case num =>
stack.push(num.toDouble)
}
}
stack.pop()
}
val result = evaluateRPN(List("3", "4", "+", "2", "*"))
// Calculates: (3 + 4) * 2 = 14
// Result: 14.0
mutable.PriorityQueue — Priority-Based Ordering
PriorityQueue is a mutable collection where elements are ordered by priority rather than insertion order. When you enqueue elements, they're automatically ordered; when you dequeue, you get the highest-priority element first. By default, PriorityQueue is a max-heap (larger elements have higher priority), but you can reverse this with a custom Ordering. PriorityQueues are essential for algorithms like Dijkstra's shortest path, task scheduling with priorities, and event processing where you want to handle the most important items first. The implementation typically uses a binary heap, providing O(log n) insertion and deletion, O(1) access to the top element. PriorityQueues are mutable because heap restructuring requires modifying elements in-place.
import scala.collection.mutable
// Create with default ordering (min-heap)
val pq = mutable.PriorityQueue[Int]()
pq.enqueue(3)
pq.enqueue(1)
pq.enqueue(4)
pq.enqueue(1)
// Priority ordering applied internally
// Dequeue returns highest priority (largest, by default)
val max1 = pq.dequeue() // Result: 4
val max2 = pq.dequeue() // Result: 3
val max3 = pq.dequeue() // Result: 1
// Custom ordering - min-heap
val minHeap = mutable.PriorityQueue[Int]()(Ordering[Int].reverse)
minHeap.enqueue(5)
minHeap.enqueue(2)
minHeap.enqueue(8)
val min1 = minHeap.dequeue() // Result: 2
val min2 = minHeap.dequeue() // Result: 5
// Real-world: Task scheduler with priorities
case class Task(id: String, priority: Int, description: String)
implicit val taskOrdering: Ordering[Task] = Ordering.by[Task, Int](_.priority).reverse
val scheduler = mutable.PriorityQueue[Task]()
scheduler.enqueue(Task("T1", 3, "Low priority task"))
scheduler.enqueue(Task("T2", 1, "Critical task"))
scheduler.enqueue(Task("T3", 2, "Medium task"))
while (scheduler.nonEmpty) {
val task = scheduler.dequeue()
println(s"Executing: ${task.id} - ${task.description}")
}
// Output (in priority order):
// Executing: T2 - Critical task
// Executing: T3 - Medium task
// Executing: T1 - Low priority task
Converting Between Mutable and Immutable
Converting between mutable and immutable collections is straightforward and essential for maintaining boundaries. Every mutable collection provides a .toList, .toVector, .toMap, or .toSet method for conversion. Similarly, immutable collections provide .toBuffer, .to(mutable.ArrayBuffer), and similar methods. The conversion is typically O(n) (you're copying elements), except for ListBuffer conversion which is O(1) amortized (the underlying linked structure is reused). Strategic conversion is your tool for containing mutation: use mutable collections internally, convert to immutable at function boundaries, and accept that the conversion cost is worth the correctness and clarity gains.
// Mutable to Immutable
val mutableList = scala.collection.mutable.ListBuffer(1, 2, 3)
val immutableList = mutableList.toList
// Result: List(1, 2, 3)
val mutableMap = scala.collection.mutable.Map("a" -> 1, "b" -> 2)
val immutableMap = mutableMap.toMap
// Result: Map(a -> 1, b -> 2)
val mutableSet = scala.collection.mutable.Set("x", "y", "z")
val immutableSet = mutableSet.toSet
// Result: Set(x, y, z)
// Immutable to Mutable
val list = List(1, 2, 3)
val buffer = list.to(scala.collection.mutable.ArrayBuffer)
// OR: list.toBuffer
val map = Map("a" -> 1, "b" -> 2)
val mutableMap2 = map.to(scala.collection.mutable.Map)
// Builder pattern - efficient collection construction
val builder = scala.collection.mutable.ListBuffer[String]()
builder += "a"
builder += "b"
builder += "c"
val result = builder.result() // Alias for .toList
Practical Example: LRU Cache Implementation
An LRU (Least Recently Used) cache is a fixed-size cache that evicts the least recently accessed element when capacity is reached. This pattern is fundamental in systems design—browsers implement LRU caches for page buffers, databases use them for buffer pools, CPUs use them for TLBs (translation lookaside buffers). Implementing LRU caches demonstrates both mutable collection use and careful state management. The implementation uses a LinkedHashMap (a mutable Map that maintains insertion order) to track access order. When accessing an element, we remove and re-insert it to move it to the end (marking it as most recent). When the cache exceeds capacity, we evict the first element (the least recently used). This is a perfect example of using mutable collections internally while presenting a clean, predictable interface externally.
A Least Recently Used cache evicts the oldest accessed element when capacity is reached:
import scala.collection.mutable
class LRUCache[K, V](maxSize: Int) {
private val cache = mutable.LinkedHashMap[K, V]()
def get(key: K): Option[V] = {
cache.get(key).flatMap { value =>
// Move to end (most recent) by removing and re-adding
cache -= key
cache += (key -> value)
Some(value)
}
}
def put(key: K, value: V): Unit = {
// Remove if exists (to update position)
cache -= key
cache += (key -> value)
// Evict oldest if over capacity
if (cache.size > maxSize) {
val oldest = cache.head._1
cache -= oldest
}
}
def size: Int = cache.size
def entries: List[(K, V)] = cache.toList
}
// Test the LRU Cache
val cache = new LRUCache[String, String](3)
cache.put("user-1", "Alice")
cache.put("user-2", "Bob")
cache.put("user-3", "Charlie")
// cache: (user-1, Alice), (user-2, Bob), (user-3, Charlie)
cache.get("user-1") // Access user-1, moves to end
// cache: (user-2, Bob), (user-3, Charlie), (user-1, Alice)
cache.put("user-4", "David") // Exceeds capacity
// Evicts oldest (user-2, Bob)
// cache: (user-3, Charlie), (user-1, Alice), (user-4, David)
println(cache.entries)
// Result: List((user-3, Charlie), (user-1, Alice), (user-4, David))
Practical Example: Task Scheduler with Priorities
A task scheduler manages jobs with different priorities, executing higher-priority tasks first. This example demonstrates combining multiple mutable collections (PriorityQueue for scheduling, ListBuffer for execution history) while maintaining a clean functional interface. The implicit Ordering defines priority logic: higher priority first, then older tasks first (FIFO within same priority). The scheduler demonstrates how mutable collections naturally express imperative algorithms while still providing a pure external interface (the executeAll method returns an immutable List).
import scala.collection.mutable
case class ScheduledTask(
id: String,
priority: Int,
description: String,
createdAt: Long = System.currentTimeMillis()
)
// Custom ordering: higher priority first, then older tasks first
implicit val taskOrdering: Ordering[ScheduledTask] =
Ordering.by[ScheduledTask, (Int, Long)](t => (-t.priority, t.createdAt))
class TaskScheduler {
private val queue = mutable.PriorityQueue[ScheduledTask]()
private val executed = mutable.ListBuffer[ScheduledTask]()
def submit(task: ScheduledTask): Unit = {
queue.enqueue(task)
}
def executeAll(): List[ScheduledTask] = {
while (queue.nonEmpty) {
val task = queue.dequeue()
executed += task
// In reality, would execute task here
}
executed.toList
}
def pending: List[ScheduledTask] = queue.toList.sorted
def executedCount: Int = executed.length
}
// Usage
val scheduler = new TaskScheduler()
scheduler.submit(ScheduledTask("email-1", 1, "Send welcome email"))
scheduler.submit(ScheduledTask("report-1", 3, "Generate daily report"))
scheduler.submit(ScheduledTask("cleanup-1", 2, "Clean temporary files"))
scheduler.submit(ScheduledTask("email-2", 1, "Send reminder"))
val executed = scheduler.executeAll()
executed.foreach(task => println(s"[${task.priority}] ${task.id}: ${task.description}"))
// Output (in priority order):
// [3] report-1: Generate daily report
// [2] cleanup-1: Clean temporary files
// [1] email-1: Send welcome email
// [1] email-2: Send reminder
Builder Pattern for Custom Collections
The Builder pattern combines mutable internal state with an immutable public interface. This pattern is powerful for constructing complex objects incrementally while maintaining a fluent, chainable API. The MovieCollection example uses private mutable buffers and maps to accumulate data, then exposes an addMovie method that returns this (enabling method chaining), and a build method that produces the final immutable FinalMovieCollection. This pattern is particularly useful when constructing objects with many fields or complex initialization logic. The builder's method chaining makes the construction process readable and expressive, while the final build call marks a clear transition to the immutable result.
import scala.collection.mutable
class MovieCollection {
private val titles = mutable.ListBuffer[String]()
private val ratings = mutable.Map[String, Double]()
private val genres = mutable.Map[String, Set[String]]()
def addMovie(title: String, rating: Double, movieGenres: Set[String]): this.type = {
titles += title
ratings += (title -> rating)
genres += (title -> movieGenres)
this // Return self for method chaining
}
def build(): FinalMovieCollection = {
new FinalMovieCollection(
titles.toList,
ratings.toMap,
genres.toMap
)
}
}
case class FinalMovieCollection(
titles: List[String],
ratings: Map[String, Double],
genres: Map[String, Set[String]]
)
// Usage with fluent interface
val collection = new MovieCollection()
.addMovie("Inception", 8.8, Set("sci-fi", "thriller"))
.addMovie("Interstellar", 8.6, Set("sci-fi", "drama"))
.addMovie("The Matrix", 8.7, Set("sci-fi", "action"))
.build()
println(collection.titles)
// Result: List(Inception, Interstellar, The Matrix)
Performance Considerations
Understanding the performance trade-offs between mutable collections is essential for choosing the right tool. ArrayBuffer excels at appending (O(1) amortized) and indexed access (O(1)), but struggles with front insertion (O(n)). ListBuffer is efficient at both ends (O(1) for prepend and append) but doesn't support indexed access efficiently (O(n)). Mutable Maps offer O(1) average-case operations for all core operations. The table below summarizes these trade-offs:
Operation ArrayBuffer ListBuffer mutable.Map Performance
----------- ----------- ---------- ----------- -----------
Add to end (+=) O(1) amortized O(1) O(1) avg ArrayBuffer best
Add to front (+=:) O(n) O(1) - ListBuffer best
Remove O(n) O(n) O(1) avg Map best
Index access O(1) O(n) O(1) avg ArrayBuffer best
Conversion to List O(1)* O(1) - ListBuffer cheapest
Memory High Low High ListBuffer best
*ListBuffer.toList is O(1) amortized; ArrayBuffer.toList requires copying
Guidelines:
- ArrayBuffer: Default for building indexed sequences iteratively
- ListBuffer: When you primarily use head/tail operations and prepend
- mutable.Map: When you need frequent updates and lookups
- mutable.Set: When tracking membership and allowing duplicates
- Queue/Stack: When order of operations matters (FIFO/LIFO)
- PriorityQueue: When priority-based ordering is essential
Best Practices for Mutable Collections
Four patterns ensure mutable collections enhance rather than undermine code quality. First, maintain a clear boundary between mutable internal state and immutable public interfaces. Functions should accept immutable types as parameters and return immutable results; mutation occurs only internally. Second, limit mutation scope—keep mutable collections as local variables within functions or private fields within classes, never exposing them in public APIs. Third, convert to immutable before returning from functions; this makes the functional contract explicit and prevents external callers from accidentally mutating your internals. Fourth, document mutation clearly—add comments explaining why mutation was necessary and what benefits it provides, helping readers understand the trade-off.
// Pattern 1: Internal mutability, immutable boundary
def processMovies(titles: List[String]): List[String] = {
val buffer = scala.collection.mutable.ListBuffer[String]()
for (title <- titles) {
// Process...
buffer += title.toUpperCase
}
buffer.toList // Always return immutable
}
// Pattern 2: Limiting scope of mutation
def analyze(data: List[Int]): Map[Int, Int] = {
val counts = scala.collection.mutable.Map[Int, Int]()
for (value <- data) {
counts(value) = counts.getOrElse(value, 0) + 1
}
counts.toMap // Conversion makes boundary clear
}
// Pattern 3: Encapsulation in classes
class WorkingSet[T] {
private val elements = scala.collection.mutable.Set[T]()
def add(elem: T): Unit = elements += elem
def contains(elem: T): Boolean = elements(elem)
def snapshot: Set[T] = elements.toSet // Immutable snapshot
}
// Pattern 4: Clear documentation
/**
* Builds a movie inventory.
* Note: Uses mutable.ArrayBuffer internally for efficiency.
* Returns an immutable List.
*/
def buildInventory(titles: List[String]): List[String] = {
val inventory = scala.collection.mutable.ArrayBuffer[String]()
for (title <- titles) {
if (title.nonEmpty) {
inventory += title
}
}
inventory.toList
}
Summary
Mutable collections in Scala are powerful tools for efficient incremental construction, imperative algorithms that demand state tracking, and performance-critical inner loops. Used correctly—internally and temporarily—they integrate seamlessly with Scala's immutability principles. Always convert to immutable before crossing function boundaries. This keeps your public API clean, thread-safe, and predictable. The pragmatic approach acknowledges that sometimes mutation is the right tool, while ensuring it doesn't compromise the integrity of your codebase's overall design.