Scala - Variables (val vs var)
• Scala enforces immutability by default through `val`, which creates read-only references that cannot be reassigned after initialization, leading to safer concurrent code and easier reasoning about...
Key Insights
• Scala enforces immutability by default through val, which creates read-only references that cannot be reassigned after initialization, leading to safer concurrent code and easier reasoning about program state.
• The var keyword enables mutable variables but should be used sparingly—prefer immutable data structures and transformation patterns over reassignment to align with functional programming principles.
• Understanding the distinction between reference immutability (val) and object immutability is critical: a val can reference a mutable collection whose contents can change, even though the reference itself cannot be reassigned.
Understanding val: Immutable References
In Scala, val declares an immutable reference. Once assigned, you cannot reassign a new value to a val. This is Scala’s default and recommended approach for variable declaration.
val name: String = "Application Architect"
val count: Int = 42
val ratio = 3.14 // Type inference works automatically
// This will cause a compilation error
// name = "New Name" // Error: reassignment to val
The immutability enforced by val provides several advantages. First, it makes code easier to reason about—once you see a val declaration, you know that reference will never point to a different value. Second, it enables safer concurrent programming since immutable references eliminate race conditions related to reassignment.
class UserService {
val database: Database = new PostgresDatabase()
val cacheTimeout: Int = 3600
def fetchUser(id: Long): User = {
// database reference never changes
database.findById(id)
}
}
Working with var: Mutable Variables
The var keyword declares a mutable variable that can be reassigned. While Scala supports var, the language philosophy encourages minimizing its use.
var counter: Int = 0
var message: String = "Initial"
counter = counter + 1 // Valid reassignment
message = "Updated" // Valid reassignment
// Type must remain consistent
// counter = "text" // Error: type mismatch
Common legitimate use cases for var include performance-critical loops, maintaining local state in algorithms, and interoperating with Java libraries that expect mutable state.
def sumArray(numbers: Array[Int]): Int = {
var sum = 0
var i = 0
while (i < numbers.length) {
sum += numbers(i)
i += 1
}
sum
}
However, the functional alternative is often clearer:
def sumArray(numbers: Array[Int]): Int = {
numbers.foldLeft(0)(_ + _)
}
Reference Immutability vs Object Immutability
A critical distinction exists between an immutable reference and an immutable object. A val creates an immutable reference, but the object it points to may still be mutable.
import scala.collection.mutable.ArrayBuffer
val mutableList = ArrayBuffer(1, 2, 3)
mutableList += 4 // Valid: modifying the object's contents
mutableList += 5 // The reference is immutable, but the object is mutable
// This would fail:
// mutableList = ArrayBuffer(6, 7, 8) // Error: reassignment to val
val immutableList = List(1, 2, 3)
// immutableList += 4 // Error: no += method on immutable List
val newList = immutableList :+ 4 // Creates a new list
For true immutability, use both val and immutable data structures:
val users: List[String] = List("Alice", "Bob")
val updatedUsers: List[String] = users :+ "Charlie" // New list
val config: Map[String, String] = Map(
"host" -> "localhost",
"port" -> "8080"
)
val newConfig = config + ("timeout" -> "30") // New map
Type Annotations and Inference
Both val and var support type inference, but explicit type annotations improve code clarity for public APIs and complex types.
// Type inference
val count = 42 // Int inferred
val name = "Scala" // String inferred
val items = List(1, 2, 3) // List[Int] inferred
// Explicit types for clarity
val timeout: Long = 5000
val handler: RequestHandler = new DefaultRequestHandler()
val cache: Map[String, User] = Map.empty
// Necessary when inference is ambiguous
val emptyList: List[String] = List.empty // Without type, List[Nothing]
Lazy Evaluation with lazy val
Scala provides lazy val for deferred initialization. The value is computed only when first accessed, useful for expensive operations or resolving circular dependencies.
class DatabaseConnection {
lazy val connection: Connection = {
println("Establishing connection...")
// Expensive operation happens only on first access
DriverManager.getConnection("jdbc:postgresql://localhost/db")
}
def query(sql: String): ResultSet = {
connection.createStatement().executeQuery(sql) // Connection initialized here
}
}
val db = new DatabaseConnection()
// "Establishing connection..." not printed yet
db.query("SELECT * FROM users") // Now it prints and connects
Lazy vals are thread-safe but have synchronization overhead. Don’t overuse them:
object Configuration {
lazy val apiKey: String = sys.env.getOrElse("API_KEY", "default")
lazy val database: Database = Database.connect(apiKey)
// Regular val for simple values
val maxRetries: Int = 3
}
Patterns for Minimizing var Usage
Replace local var with functional transformations:
// Imperative style with var
def processItems(items: List[Int]): List[Int] = {
var result = List.empty[Int]
for (item <- items) {
if (item > 0) {
result = result :+ (item * 2)
}
}
result
}
// Functional style with val
def processItems(items: List[Int]): List[Int] = {
items.filter(_ > 0).map(_ * 2)
}
Use recursion instead of loops with counters:
// With var
def factorial(n: Int): Int = {
var result = 1
var i = 1
while (i <= n) {
result *= i
i += 1
}
result
}
// Tail-recursive with val
def factorial(n: Int): Int = {
@scala.annotation.tailrec
def loop(current: Int, accumulator: Int): Int = {
if (current <= 0) accumulator
else loop(current - 1, current * accumulator)
}
loop(n, 1)
}
Practical Guidelines
Choose val by default. Only use var when you have a specific reason:
class ShoppingCart {
private var items: List[Item] = List.empty // Mutable state encapsulated
def addItem(item: Item): Unit = {
items = items :+ item
}
def total: BigDecimal = items.map(_.price).sum
}
For class fields, prefer immutable case classes:
// Mutable approach (avoid)
class User {
var name: String = ""
var email: String = ""
}
// Immutable approach (preferred)
case class User(name: String, email: String)
// Updates create new instances
val user = User("Alice", "alice@example.com")
val updatedUser = user.copy(email = "newemail@example.com")
When interacting with Java libraries that require mutable state, isolate var usage:
import java.util.{ArrayList => JList}
def convertToJavaList(items: List[String]): JList[String] = {
val javaList = new JList[String]()
items.foreach(javaList.add) // Mutation isolated to this function
javaList
}
Performance Considerations
Immutable collections use structural sharing, making copying operations efficient:
val list1 = List(1, 2, 3)
val list2 = 0 :: list1 // O(1) operation, shares structure
// Mutable alternative
val buffer = scala.collection.mutable.ListBuffer(1, 2, 3)
buffer.prepend(0) // In-place mutation
For tight loops with millions of iterations, var may offer performance benefits, but measure first:
def efficientSum(array: Array[Int]): Long = {
var sum = 0L
var i = 0
while (i < array.length) {
sum += array(i)
i += 1
}
sum
}
The compiler and JVM optimize immutable code effectively. Premature optimization toward mutability often sacrifices maintainability for negligible gains.