(3) var, val and lazy

About

:octocat: GitHub: All of the example code: repo (link)

:page_facing_up: blog link: https://purrgramming.life/cs/programming/fp/ :star:


var vs val

In Java, you declare new variables like this:

String s = "hello";
int i = 42;
Person p = new Person("Victoria Pinzhen Liao");

Each variable declaration is preceded by its type.

Scala has two types of variables:

  • val creates a binding that cannot be reassigned, like a Java final variable. It need not be a compile-time constant, and an object reached through it can still be mutable.
  • var creates a mutable variable

This is what variable declaration looks like in Scala:

//  variable’s type is  _inferred_  by the compiler 
val s = "hello"   // immutable
var i = 42        // mutable

class Person(name: String)
val p = new Person("Victoria Pinzhen Liao")

Those examples show that the Scala compiler is usually smart enough to infer the variable’s data type from the code on the right side of the = sign. We say that the variable’s type is inferred by the compiler.

You can also explicitly declare the variable type if you prefer:

//  _explicitly_  declare the variable type 
val s: String = "hello"
var i: Int = 42

Performance

val and var are evaluated when defined.

val vs var

val prevents reassignment of the binding; var allows it. This is separate from whether the referenced object can change: a val holding a mutable buffer still permits updates to that buffer.

val a = 'a'
a = 'b'

Result

<console>:12: error: reassignment to val

That fails with a reassignment to val error, as expected. Conversely, you can reassign a var:

var c = 'c'  
c = 'd' // This is fine

Result

c: Char = d

Which One?

The general rule is that you should always use a val field unless there is a good reason not to.

  • makes your code more like algebra
  • helps get you started down the path to functional programming, where all fields are immutable.

val in REPL

The REPL is not 100% the same as working with source code in an IDE, so you can do a few things in the REPL that you cannot do when working on real-world code in a project.

You can redefine a val field in the REPL, like this:

// It's ok only in REPL
scala> val age = 18
age: Int = 18

scala> val age = 19
age: Int = 19

These REPL entries introduce successive bindings; they do not mutate the original val. A source file cannot declare the same name twice in the same scope, although a nested scope can shadow an outer binding.

val vs def

def declares a method: its body runs when the method is invoked.

def introduces a definition where the right-hand side is evaluated on each use.

While the def is a function declaration, it is evaluated on call, i.e. val evaluates when defined, def – when called:

Example:

// Complain immediately
scala> val even: Int => Boolean = ???
scala.NotImplementedError: an implementation is missing

// Complain on call
scala> def even: Int => Boolean = ???
even: Int => Boolean

scala> even
scala.NotImplementedError: an implementation is missing

Function Identity

A method returning a function evaluates its body on each invocation. Do not infer that every invocation must allocate a distinct function object: compilers and runtimes may reuse non-capturing functions. Reference identity is not a reliable way to distinguish def from val.

def even: Int => Boolean = _ % 2 == 0  
even eq even  
// Result can depend on compiler/runtime; do not rely on identity here.  

val evenVal: Int => Boolean = _ % 2 == 0  
evenVal eq evenVal  
//Boolean = true  

Function Results

With def you can get new function on every call:

val randomInt: () => Int = {  
  val r = util.Random.nextInt  
  () => r  
}  

randomInt() // val res3: Int = 1764655189  
randomInt() // val res4: Int = 1764655189

// --------------------------------  

def randomIntDef: () => Int = {  
  val r = util.Random.nextInt  
  () => r  
}  

// Different  
randomIntDef()  
randomIntDef()

lazy val

lazy val is evaluated when called the first time:

scala> lazy val even: Int => Boolean = ???
even: Int => Boolean = <lazy>

scala> even
scala.NotImplementedError: an implementation is missing

But returns the same result (in this case same instance of FunctionN) every time:

Identity

lazy val even: Int => Boolean = _ % 2 == 0  
even eq even  
//Boolean = true

Results


lazy val randomInt: () => Int = {  
  val r = util.Random.nextInt  
  () => r  
}  

randomInt()  
// Int = -1068569869  
randomInt()  
// Int = -1068569869 - same result

Performance

val is evaluated when defined.

A def can repeat work; a strict val computes its initializer when its declaration is evaluated. Actual performance depends on the work, compiler optimization and call pattern, so neither equal single-call performance nor a universal winner follows from the keyword alone.

A lazy val defers initialization until first access and caches a successfully initialized value. If initialization throws, a later access may retry. Initialization coordination has a cost, but its implementation differs across Scala versions; it is not accurately described as double-checked locking on every access.

Passing a method where a function value is expected uses eta expansion. Allocation and reuse are compiler/runtime details; benchmark the real workload before choosing a declaration for performance.

Takeaway

Choose val for a stable binding, var for deliberate reassignment, def for an operation and lazy val for a deferred, cached result. Distinguish binding stability, object mutability and evaluation timing.

References: Scala variables; Scala 3 lazy-value initialization.

2 thoughts on “(3) var, val and lazy”

  1. Victoria Liao Post author

    Hi Bob : )

    If you can reproduce this in the workspace with

    def even: Int => Boolean = _ % 2 == 0
    even eq even
    // Workspace result:

    // def even: Int => Boolean
    // val res1: Boolean = false

Leave a Reply

Your email address will not be published. Required fields are marked *