As val
are semantically static, they are initialized "in-place" wherever they appear in the code. This can produce surprising and undesirable behavior when used in abstract classes and traits.
For example, let's say we would like to make a trait called PlusOne
that defines an increment operation on a wrapped Int
. Since Int
s are immutable, the value plus one is known at initialization and will never be changed afterwards, so semantically it's a val
. However, defining it this way will produce an unexpected result.
trait PlusOne {
val i:Int
val incr = i + 1
}
class IntWrapper(val i: Int) extends PlusOne
No matter what value i
you construct IntWrapper
with, calling .incr
on the returned object will always return 1. This is because the val incr
is initialized in the trait, before the extending class, and at that time i
only has the default value of 0
. (In other conditions, it might be populated with Nil
, null
, or a similar default.)
The general rule, then, is to avoid using val
on any value that depends on an abstract field. Instead, use lazy val
, which does not evaluate until it is needed, or def
, which evaluates every time it is called. Note however that if the lazy val
is forced to evaluate by a val
before initialization completes, the same error will occur.
A fiddle (written in Scala-Js, but the same behavior applies) can be found here.