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:
valcreates a binding that cannot be reassigned, like a Javafinalvariable. It need not be a compile-time constant, and an object reached through it can still be mutable.varcreates 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.
This is true
Hi Bob : )
If you can reproduce this in the workspace with
def even: Int => Boolean = _ % 2 == 0even eq even
// Workspace result:
// def even: Int => Boolean
// val res1: Boolean = false