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.

Liked this? There's more.

Every week: one practical technique, explained simply, with code you can use immediately.