Scala 3 Module 2 – Basics: values, numbers, text and control flow
val, var, lazy val and type inference
Choose between val, var, lazy val and def in Scala 3, predict the types the compiler infers, such as Int | String, and write the types an API needs.
What you will learn
- Choose between val, var, lazy val and def for each definition in a program
- Predict when a lazy val runs its right-hand side, also with threads and exceptions
- Predict the type the compiler infers, including union and literal types
- Write result types for recursive methods and public members to keep an API stable
Before you start
What you’ll learn · Before you start
What you will learn
- Choose between val, var, lazy val and def for each definition in a program
- Predict when a lazy val runs its right-hand side, also with threads and exceptions
- Predict the type the compiler infers, including union and literal types
- Write result types for recursive methods and public members to keep an API stable
Before you start
On this page
In the REPL session of this lesson, one line of code ran a def’s body twice, a lazy val ran its body once and
only on its first use, and var best = None refused the Some(42) given to it on the next line. A value you name in
Scala is introduced with val, var, lazy val or def, and the keyword decides two things: when the right-hand
side runs, and whether the name can ever point somewhere else.
This lesson shows each keyword at work in recorded runs. It then reads the types the compiler works out by itself,
including union types such as Int | String and literal types such as 180, and ends with the places where writing a
type yourself moves a confusing error in a caller to the line that is actually wrong.
val names a value once, and var can be reassigned
A booking program needs a seat count that never changes, a count of bookings that grows, a basket of items and a list
of prices. Here are all four, followed by a second file that tries to give two vals a new value:
import scala.collection.mutable.ArrayBuffer
@main def bindings(): Unit =
val seats = 180
var booked = 0
booked = booked + 3
booked += 2
println(s"booked $booked of $seats")
val basket = ArrayBuffer("pen", "pad")
basket += "ink"
println(s"basket: $basket")
val prices = List(40, 25)
val more = prices :+ 60
println(s"prices: $prices, more: $more") import scala.collection.mutable.ArrayBuffer
@main def reassign(): Unit =
val seats = 180
seats = 200
val basket = ArrayBuffer("pen", "pad")
basket = ArrayBuffer("ink") from session import run
# Bindings.scala runs; Reassign.scala tries to give two vals a new value and does not compile.
run("""
cd bindings
scala run Bindings.scala --server=false
!scala compile Reassign.scala --server=false
""") Output
$ cd bindings
$ scala run Bindings.scala --server=false
booked 5 of 180
basket: ArrayBuffer(pen, pad, ink)
prices: List(40, 25), more: List(40, 25, 60)
$ scala compile Reassign.scala --server=false
-- [E052] Type Error: Reassign.scala:5:8
5 | seats = 200
| ^^^^^^^^^^^
| Reassignment to val seats
|
| longer explanation available when compiling with `-explain`
-- [E052] Type Error: Reassign.scala:7:9
7 | basket = ArrayBuffer("ink")
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^
| Reassignment to val basket
|
| longer explanation available when compiling with `-explain`
2 errors found
Compilation failed
[exit status 1]
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 bindings_session.py
val seats = 180 makes seats a name for 180 for as long as it is in scope. var booked = 0 makes a name that can be
pointed at a new value, and booked = booked + 3 does exactly that: the right-hand side is worked out first, and the
name moves to the result. += is the same reassignment, written shorter. The Scala 3 Book’s advice is to define every
name with val unless there is a reason to change it, and the compiler holds a val to its promise: both lines of
Reassign.scala stop with [E052] Reassignment to val.
The first program also shows what a val does not promise. basket += "ink" compiled and ran, because it changes the
ArrayBuffer that basket points to, not the name. Only basket = ArrayBuffer("ink"), pointing the name at another
buffer, is refused. A List is different again: it can never change, so prices :+ 60 built a new list and left
prices with its two items. Step through the program and watch which arrows move and which objects change:
The drawing shows the whole scene; its 8 steps are listed below.
Every step as text
- val seats = 180. The name seats now stands for 180 for as long as it is in scope. A val is a promise to the compiler that this name never points anywhere else.
- var booked = 0. A var is a name that may be pointed at a new value later. It starts at 0.
- booked = booked + 3. The right-hand side is worked out first, 0 + 3, and booked is pointed at the result. The value 0 itself did not change; the name moved.
- booked += 2. += is the same reassignment written shorter, booked = booked + 2. The program prints booked 5 of 180.
- val basket. basket is a val, but the object it points to is an ArrayBuffer, a buffer you can change in place.
- basket += "ink". This line changes the buffer, not the name. basket still points to the same buffer, which now holds three items. A val fixes the binding, not the object.
- basket = ArrayBuffer("ink"). Pointing the val at a different buffer is what val forbids. Reassign.scala tries it, and the compiler stops with E052, Reassignment to val basket.
- val more = prices :+ 60. A List cannot change. :+ builds a new list with 60 at the end and leaves prices as it was, so the program prints both.
Reading val as a frozen object
val basket = ArrayBuffer("pen", "pad") does not stop basket += "ink"; it only stops basket = …. When the data
itself must not change, choose an immutable type such as List or Vector, and keep ArrayBuffer for code that
changes a buffer on purpose.
Checkpoint
Check: val, var and what they fix
A def runs on every use, a lazy val on the first
The keyword also decides when the right-hand side runs. The session below types six lines into the REPL, and each right-hand side prints a line when it runs:
from session import repl
# When the right-hand side runs: a val at once, a lazy val on its first use, a def on every use.
repl([
'val seats = { println("val: computing seats"); 180 }',
'lazy val rates = { println("lazy val: loading rates"); List(5, 12, 18) }',
"rates.sum",
"rates.max",
'def stamp = { println("def: computing stamp"); seats * 2 }',
"stamp + stamp",
":quit",
]) Output
$ scala repl --server=false
Welcome to Scala 3.9.0 (25.0.4.1, Java OpenJDK 64-Bit Server VM).
Type in expressions for evaluation. Or try :help.
scala> val seats = { println("val: computing seats"); 180 }
val: computing seats
val seats: Int = 180
scala> lazy val rates = { println("lazy val: loading rates"); List(5, 12, 18) }
lazy val rates: List[Int]
scala> rates.sum
lazy val: loading rates
val res0: Int = 35
scala> rates.max
val res1: Int = 18
scala> def stamp = { println("def: computing stamp"); seats * 2 }
def stamp: Int
scala> stamp + stamp
def: computing stamp
def: computing stamp
val res2: Int = 720
scala> :quit
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 timing_session.py
The val printed val: computing seats at once, before the REPL even showed its type. The
lazy val: A val whose right-hand side runs on its first use rather than where it is defined. The result is kept for later uses; if the right-hand side throws, the next use runs it again. printed nothing where it was defined, and the REPL answered with its type alone,
lazy val rates: List[Int], because there was no value yet; its first use, rates.sum, ran the body, and
rates.max reused the kept list in silence. The def is a method: it keeps nothing, so stamp + stamp ran its body
twice. Step through the six lines:
The drawing shows the whole scene; its 7 steps are listed below.
Every step as text
- val seats. A val runs its right-hand side at once, where it is defined. The REPL printed val, computing seats before it even answered with the type.
- lazy val rates. Defining the lazy val runs nothing. The REPL answers lazy val rates with its type and no value, because there is no value yet.
- rates.sum. The first use runs the right-hand side. It prints lazy val, loading rates, keeps the list and hands it to sum, which gives 35.
- rates.max. The second use reads the kept list. Nothing is printed this time, and max gives 18.
- def stamp. A def is a method. Defining it runs nothing, and nothing is kept.
- first call. stamp + stamp calls the method twice. The first call runs the body, prints def, computing stamp and gives 360.
- second call. The second call runs the body again and prints the same line again. The sum is 720, and the body ran twice for one line of code.
A lazy val suits a value that is costly to build and may never be needed, such as a table read from a file. The
next program puts one in an object, has eight threads read it at the same moment, and then uses a second lazy val
whose first attempt throws an exception:
import java.util.concurrent.atomic.AtomicInteger
object Config:
val loads = AtomicInteger()
lazy val rates: Map[String, Int] =
loads.incrementAndGet()
println("reading the rate table")
Thread.sleep(50)
Map("food" -> 5, "books" -> 12, "phones" -> 18)
var tries = 0
lazy val port: Int =
tries += 1
if tries == 1 then throw RuntimeException("settings not ready")
8080
@main def readConfig(): Unit =
println("program started")
val seen = new Array[Int](8)
val threads = (0 until 8).map(i => Thread(() => seen(i) = Config.rates("books")))
threads.foreach(_.start())
threads.foreach(_.join())
println(s"8 threads read ${seen.distinct.mkString(", ")}")
println(s"phones: ${Config.rates("phones")}")
println(s"the table was read ${Config.loads.get} time")
try println(port)
catch case e: RuntimeException => println(s"first use failed: ${e.getMessage}")
println(s"second use: $port, after $tries tries") from session import run
# A lazy val in an object, read by eight threads at once, then a lazy val whose first initialisation throws.
run("""
cd config
scala run Config.scala --server=false
""") Output
$ cd config $ scala run Config.scala --server=false program started reading the rate table 8 threads read 12 phones: 18 the table was read 1 time first use failed: settings not ready second use: 8080, after 2 tries
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 config_session.py
The table was read once, although eight threads asked for it together: a lazy val’s initialisation is guarded, so the
other threads wait for the first one and then read its result. The annotation @threadUnsafe gives up that guard for
a faster mechanism, so keep it for values that only one thread ever reads. The second part shows a rule of the
language specification: when the right-hand side throws, the value is still not initialised, and the next use runs
it again. The first use of port failed, and the second returned 8080 after two tries.
| Keyword | The right-hand side runs | The name can be reassigned | Typical use |
|---|---|---|---|
val |
once, where it is defined | no | almost everything |
var |
once, and again at each reassignment | yes | a counter in a loop, a running total |
lazy val |
once, on the first use; again after a failure | no | a costly value that may not be needed |
def |
on every use | no, it is a method | a value that must be computed fresh each time |
Lazy vals and newer JDKs
Since Scala 3.8, lazy vals compile to code that uses VarHandle instead of sun.misc.Unsafe, which JDK 24 and
later warn about at run time. Libraries compiled with Scala 3.0 to 3.7 still use the old path, so a warning about
sun.misc.Unsafe on a new JDK usually comes from a dependency, not from your own lazy vals.
Checkpoint
Check: When val, lazy val and def run
The compiler infers each type from the right-hand side
None of the definitions so far named a type, yet every one has one. The compiler’s type inference: The compiler working out a type you did not write. Scala infers the type of each definition from its right-hand side alone, so the first value of a var fixes its type. works it out from the right-hand side, and the REPL prints what it found. This session types thirteen lines, two of them meant to fail:
from session import repl
# The types the compiler infers, read from the REPL's answers: an Int literal adapted to Double where an Int value is
# not, and the var that a None fixes to None.type.
repl([
"val seats = 180",
"val share = seats / 7.0",
"val prices = List(40, 25.5)",
"val mixed = List(seats, 25.5)",
'val discount = if seats > 100 then 10 else "none"',
"val offer = if seats > 100 then Some(10) else None",
"var best = None",
"best = Some(42)",
"var kept: Option[Int] = None",
"kept = Some(42)",
"object Limits { final val MaxSeats = 180 }",
"val limit: 180 = Limits.MaxSeats",
"val other: 180 = seats",
":quit",
]) Output
$ scala repl --server=false
Welcome to Scala 3.9.0 (25.0.4.1, Java OpenJDK 64-Bit Server VM).
Type in expressions for evaluation. Or try :help.
scala> val seats = 180
val seats: Int = 180
scala> val share = seats / 7.0
val share: Double = 25.714285714285715
scala> val prices = List(40, 25.5)
val prices: List[Double] = List(40.0, 25.5)
scala> val mixed = List(seats, 25.5)
val mixed: List[Int | Double] = List(180, 25.5)
scala> val discount = if seats > 100 then 10 else "none"
val discount: Int | String = 10
scala> val offer = if seats > 100 then Some(10) else None
val offer: Option[Int] = Some(10)
scala> var best = None
var best: None.type = None
scala> best = Some(42)
-- [E007] Type Mismatch Error: -------------------------------------------------
1 |best = Some(42)
| ^^^^^^^^
| Found: Some[Int]
| Required: None.type
|
| longer explanation available when compiling with `-explain`
1 error found
scala> var kept: Option[Int] = None
var kept: Option[Int] = None
scala> kept = Some(42)
kept: Option[Int] = Some(42)
scala> object Limits { final val MaxSeats = 180 }
// defined object Limits
scala> val limit: 180 = Limits.MaxSeats
val limit: 180 = 180
scala> val other: 180 = seats
-- [E007] Type Mismatch Error: -------------------------------------------------
1 |val other: 180 = seats
| ^^^^^
| Found: (seats : Int)
| Required: (180 : Int)
|
| longer explanation available when compiling with `-explain`
1 error found
scala> :quit
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 inference_session.py
Read the answers one at a time. seats is an Int, and dividing it by 7.0, a Double, gives a Double.
List(40, 25.5) became a List[Double]: Scala adapts an Int literal such as 40 to the numeric type the other
values need, so the list holds 40.0. The rule is for literals only: List(seats, 25.5), with the Int value
seats, became a List[Int | Double], a kind of type the next section explains.
if seats > 100 then Some(10) else None is an Option[Int], because Some[Int] and None share that parent.
Inference is local: it reads the right-hand side of one definition and nothing that comes after it.
That is why var best = None went wrong. Its right-hand side is the object None, so the compiler gave best the
type None.type, the type of None alone, and the next line, best = Some(42), was refused: Found: Some[Int],
Required: None.type. Writing the type, var kept: Option[Int] = None, tells the compiler what the variable will
hold later, and kept = Some(42) then works.
var best = None
A var whose first value is narrower than what it will hold later, such as None, Nil or 0 for a total that
will need a Long, gets the narrow type. Write the type on such a var: var best: Option[Int] = None.
Checkpoint
Check: Types the compiler infers
Union and literal types show what the compiler knows
Two answers of the same session need a closer look. if seats > 100 then 10 else "none" has the type Int | String,
a union type: A type written A | B, whose values are either an A or a B. Scala 3 infers one when the branches of an if or a match share no parent but root types such as Any.: its value is either an Int or a String. The only parents the two
branches share are root types such as Any and Matchable, and the Scala 3 Reference counts those as transparent:
inference does not widen a union to them. So the compiler keeps Int | String rather than the widened type Any, and
the type says exactly what the expression can hold. List(seats, 25.5) kept the element type Int | Double for the
same reason: an Int and a Double share only transparent parents such as AnyVal. A union of a number and a word
is usually a mistake, and the next section shows where it surfaces.
The other is a literal type: A type with exactly one value, written as that value: the type 180 holds only the number 180. A final val in an object keeps its literal type; a plain val widens it to Int.: a type with exactly one value, written as that value. 180
is the type whose only value is the number 180. A final val in an object keeps it, so val limit: 180 = Limits.MaxSeats compiled; a plain val widens it to Int, so val other: 180 = seats was refused with Found: (seats : Int) and Required: (180 : Int). In such messages, (seats : Int) means “the value seats, of type Int”,
and (180 : Int) means “the number 180, as a type”.
| You write | The compiler infers |
|---|---|
val share = seats / 7.0 |
Double |
val prices = List(40, 25.5) |
List[Double] |
val mixed = List(seats, 25.5) |
List[Int | Double] |
val offer = if seats > 100 then Some(10) else None |
Option[Int] |
val discount = if seats > 100 then 10 else "none" |
Int | String |
var best = None |
None.type |
final val MaxSeats = 180, inside an object |
180 |
Key idea
The compiler infers the type of each definition from its right-hand side alone: a common parent when the branches
have one, a union such as Int | String when they do not, and the narrow type of the first value for a var.
Checkpoint
Check: Union and literal types
Write the type where inference stops or an API begins
Inference has one hard stop. A method that calls itself needs its result type, because the compiler would need the type to work out the type:
def lateFee(days: Int) =
if days <= 0 then 0 else 50 + lateFee(days - 1) Writing def lateFee(days: Int): Int = fixes it. The other place for a written type is any member that other code
uses. Here a discount method has gained a branch that returns the text "none", and a second file uses the method:
object Prices:
def discount(code: String) =
if code == "STUDENT" then 10
else if code == "STAFF" then 15
else "none" @main def checkout(code: String): Unit =
val price = 500
val toPay = price - price * Prices.discount(code) / 100
println(s"to pay: $toPay") The session compiles this folder, then a copy whose discount declares its result type, : Int, and finally
LateFee.scala:
from session import run
# The same mistake in Prices.scala, with the result type inferred and then declared; then a recursive method
# without a result type.
run("""
cd api-inferred
!scala compile . --server=false
cd ../api-declared
!scala compile . --server=false
cd ../recursive
!scala compile LateFee.scala --server=false
""") Output
$ cd api-inferred
$ scala compile . --server=false
-- [E134] Type Error: Checkout.scala:3:28
3 | val toPay = price - price * Prices.discount(code) / 100
| ^^^^^^^
| None of the overloaded alternatives of method * in class Int with types
| (x: Double): Double
| (x: Float): Float
| (x: Long): Long
| (x: Int): Int
| (x: Char): Int
| (x: Short): Int
| (x: Byte): Int
| match arguments (Int | String)
1 error found
Compilation failed
[exit status 1]
$ cd ../api-declared
$ scala compile . --server=false
-- [E007] Type Mismatch Error: Prices.scala:5:9
5 | else "none"
| ^^^^^^
| Found: ("none" : String)
| Required: Int
|
| longer explanation available when compiling with `-explain`
1 error found
Compilation failed
[exit status 1]
$ cd ../recursive
$ scala compile LateFee.scala --server=false
-- [E044] Cyclic Error: LateFee.scala:2:32
2 | if days <= 0 then 0 else 50 + lateFee(days - 1)
| ^
| Overloaded or recursive method lateFee needs return type
|
| Run with -explain-cyclic for more details.
|
| longer explanation available when compiling with `-explain`
1 error found
Compilation failed
[exit status 1]
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 api_session.py
With the type inferred, Prices.scala compiled: its result type silently became Int | String. The error appeared in
Checkout.scala, as [E134], a list of seven overloads of *, none of which takes an Int | String, far from the
mistake. With : Int written, the compiler checks the body against it, and the error is [E007] in Prices.scala on
line 5, at "none", the branch to fix. Step through both compiles:
The drawing shows the whole scene; its 5 steps are listed below.
Every step as text
- The mistake. Someone added a branch to discount that returns the text "none" where every other branch returns a number of per cent.
- Inferred type. With no result type written, the compiler infers one from the branches, Int | String, and Prices.scala compiles without a word.
- Error in the caller. The first error is in Checkout.scala, where price * Prices.discount(code) finds no * that takes an Int | String. It is E134, a list of seven overloads, far from the mistake.
- Declared type. Now discount declares its result type, Int. The type is no longer worked out from the body; the body is checked against it.
- Error at the mistake. The error moves to Prices.scala line 5, at "none". E007 says Found ("none" : String), Required Int, and names the very branch to fix.
A written result type is also the contract other code relies on: change the body, and an inferred type changes with it, quietly, for every caller. The Scala 3 Book recommends annotating every publicly visible method with its return type. Inside a method, let inference do its work.
Key idea
Write the type on recursive methods, on public members and on a var whose first value is narrower than its later
ones; everywhere else, let the compiler infer it.
Checkpoint
Check: Types you write yourself
Practise: settings that are read once
The program below reads three settings through read, which records every read. Its checks want each setting read
only when it is first used, and never more than once. As given, two settings are vals and one is a def:
// Practise: choose val, lazy val or def for each setting, so that each one is read
// only when it is first used, and never more than once.
object Settings:
var reads = List.empty[String]
private def read(name: String, value: Int): Int =
reads = reads :+ name
value
val timeoutMs = read("timeoutMs", 3_000)
val retries = read("retries", 3)
def poolSize = read("poolSize", 8)
@main def checkSettings(): Unit =
def check(label: String, ok: Boolean): Boolean =
println(s"${if ok then "ok " else "FAIL"} $label")
ok
def times(name: String) = Settings.reads.count(_ == name)
val results = List(
check("nothing is read before a setting is used", Settings.reads.isEmpty),
check("timeoutMs is 3000", Settings.timeoutMs == 3_000),
check("timeoutMs is read once", { Settings.timeoutMs; times("timeoutMs") == 1 }),
check("poolSize is read once", { Settings.poolSize; Settings.poolSize; times("poolSize") == 1 }),
check("retries, never used, is never read", times("retries") == 0),
)
println(s"${results.count(identity)} of ${results.size} checks pass")
if results.contains(false) then sys.exit(1) from session import run
# The practice program as given: two vals and a def, before you choose the right keyword for each.
run("""
cd settings
!scala run Settings.scala --server=false
""") Output
$ cd settings $ scala run Settings.scala --server=false FAIL nothing is read before a setting is used ok timeoutMs is 3000 ok timeoutMs is read once FAIL poolSize is read once FAIL retries, never used, is never read 2 of 5 checks pass [exit status 1]
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 settings_session.py
Two of the five checks pass. Copy settings/Settings.scala into an empty folder, choose val, lazy val or def
for each of the three settings, and run scala run Settings.scala until it reports 5 of 5 checks pass. First hint:
a val in an object runs when the object is first used, a def runs on every use; one keyword runs on the first use
and keeps the result.
The self-check below asks about bindings, timing, inference and written types, starts with whatever you know least, and gives the reason for every answer:
Practice that adapts to you
Practise this lesson
12 questions about 5 ideas of this lesson. Each answer is explained at once, and what you get right is kept in this browser.
Interview questions
Fresher · What is the difference between val, var, lazy val and def in Scala?
All four introduce a name, and they differ in when the right-hand side runs and whether the name can change. A val
runs its right-hand side once, where it is defined, and the name can never be pointed at another value, although the
object it points to may still be mutable, like an ArrayBuffer. A var also runs once but can be reassigned. A
lazy val runs its right-hand side on the first use and keeps the result; initialisation is thread-safe by default,
and if it throws, the next use tries again. A def is a method: its body runs on every use and nothing is kept. In
practice you use val by default, var for local state such as a counter, lazy val for a costly value that may not
be needed, and def for anything that must be computed fresh.
Key takeaways
- A
valfixes its name to one value and the compiler refuses a reassignment with E052, but the object it points to can still change; avarcan be pointed at a new value. - A
valruns its right-hand side where it is defined, alazy valon its first use, once, even with several threads, and again after a failure, and adefon every use. - The compiler infers each type from the right-hand side alone, so
var best = NonegetsNone.typeand needs a written type. - When branches have no common parent but
Any, Scala 3 infers a union such asInt | String, and afinal valin an object keeps a literal type such as180. - A recursive method needs a result type, and a written result type on a public member keeps its contract fixed and moves an error from the callers to the line that is wrong.
Terms in this lesson
- lazy val
- A val whose right-hand side runs on its first use rather than where it is defined. The result is kept for later uses; if the right-hand side throws, the next use runs it again.
- literal type
- A type with exactly one value, written as that value: the type 180 holds only the number 180. A final val in an object keeps its literal type; a plain val widens it to Int.
- type inference
- The compiler working out a type you did not write. Scala infers the type of each definition from its right-hand side alone, so the first value of a var fixes its type.
- union type
- A type written A | B, whose values are either an A or a B. Scala 3 infers one when the branches of an if or a match share no parent but root types such as Any.
References
- Scala 3 Book: variables and data types (Scala Center and EPFL (docs.scala-lang.org))
- Scala 3 Book: inferred types (Scala Center and EPFL (docs.scala-lang.org))
- Scala 3 Book: method features (return types of public methods) (Scala Center and EPFL (docs.scala-lang.org))
- Scala 3 Reference: union types (Scala Center and EPFL (docs.scala-lang.org))
- Scala 3 Reference: union types, more details (widening in inference) (Scala Center and EPFL (docs.scala-lang.org))
- Scala 3 Reference: dropped weak conformance (Int literals adapted to other numeric types) (Scala Center and EPFL (docs.scala-lang.org))
- Scala 3 Reference: transparent traits and classes (Scala Center and EPFL (docs.scala-lang.org))
- Scala 3 Reference: changes in lazy vals initialization (Scala Center and EPFL (docs.scala-lang.org))
- Scala 3 Reference: the threadUnsafe annotation (Scala Center and EPFL (docs.scala-lang.org))
- Scala Language Specification 3.4, chapter 4: basic declarations and definitions (The Scala Programming Language (scala-lang.org))
- Scala Language Specification 3.4, chapter 5: classes and objects (the lazy modifier) (The Scala Programming Language (scala-lang.org))
- Scala 3.8 release announcement (lazy vals and VarHandles) (The Scala Programming Language (scala-lang.org))
- Scala 3.9 release announcement (lazy vals compiled by older Scala versions) (The Scala Programming Language (scala-lang.org))
- Scala 3 Reference, error codes (Scala Center and EPFL (docs.scala-lang.org))
Report a problem with this lesson
Kept only in this browser. Your Learn progress