Your country

Tools that support it use your country for local currency, number formats, units and paper size. Your choice is saved only in this browser.

Type a name or a two-letter code. Use the up and down arrow keys to move through the countries, Enter to choose one and Escape to close.

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.

  • Beginner
  • 15 minutes
  • Examples run with scala 3.9.0, Java 25.0.4.1 and Python 3.14.8
  • By MySmartCoPilot
Free: 15 minutes

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:

bindings/Bindings.scala Scala · bindings/Bindings.scala
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")
bindings/Reassign.scala Scala · bindings/Reassign.scala
import scala.collection.mutable.ArrayBuffer

@main def reassign(): Unit =
  val seats = 180
  seats = 200
  val basket = ArrayBuffer("pen", "pad")
  basket = ArrayBuffer("ink")
Bindings.scala runs; Reassign.scala does not compile Python · bindings_session.py
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:

Interactive · step by step What val and var fix: the name, not the object Step through Bindings.scala. Watch which arrows move, which objects change, and which line the compiler refuses.
What val and var fix: the name, not the object, step 8 of 8. Showing val seats, 180, var booked, 0 (now 5), val basket, ArrayBuffer (pen, pad, ink), val prices, List(40, 25), val more, List(40, 25, 60). In focus: List(40, 25, 60).val seats180var booked0now 5val basketArrayBufferpen, pad, inkval pricesList(40, 25)val moreList(40, 25, 60)

The drawing shows the whole scene; its 8 steps are listed below.

Every step as text
  1. 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.
  2. var booked = 0. A var is a name that may be pointed at a new value later. It starts at 0.
  3. 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.
  4. booked += 2. += is the same reassignment written shorter, booked = booked + 2. The program prints booked 5 of 180.
  5. val basket. basket is a val, but the object it points to is an ArrayBuffer, a buffer you can change in place.
  6. 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.
  7. 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.
  8. 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

  1. Question 1 of 2 val seats = 180 and var booked = 0 are defined. Which of these lines does the compiler refuse?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: seats = 200

    A val fixes its name to one value for as long as it is in scope, so seats = 200 fails with E052. A var may be reassigned, with = or with +=.

  2. Question 2 of 2 What happens with these three lines?

    Read the code, then choose one answer.

    val basket = scala.collection.mutable.ArrayBuffer("pen")
    basket += "ink"
    println(basket)
    Show the answer: Question 2 of 2

    Answer: It prints ArrayBuffer(pen, ink)

    A val fixes the binding, not the object. basket += "ink" adds to the same ArrayBuffer, so the program prints ArrayBuffer(pen, ink). Only basket = ArrayBuffer("ink") would be refused.

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:

When a val, a lazy val and a def run Python · timing_session.py
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:

Interactive · message by message When a val, a lazy val and a def run their right-hand side Step through the six lines of the REPL session and watch which line makes each body run.
When a val, a lazy val and a def run their right-hand side, step 7 of 7. Messages so far: the REPL lines to lazy val rates: first use; the REPL lines to lazy val rates: second use; the REPL lines to def stamp: stamp; the REPL lines to def stamp: stamp again.the REPL lineswhat you typelazy val rateskept: List(5, 12, 18)def stampseats * 2first usesecond usestampstamp againbody ran 2 times

The drawing shows the whole scene; its 7 steps are listed below.

Every step as text
  1. 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.
  2. 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.
  3. 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.
  4. rates.max. The second use reads the kept list. Nothing is printed this time, and max gives 18.
  5. def stamp. A def is a method. Defining it runs nothing, and nothing is kept.
  6. first call. stamp + stamp calls the method twice. The first call runs the body, prints def, computing stamp and gives 360.
  7. 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:

config/Config.scala Scala · config/Config.scala
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")
A lazy val read by eight threads, and one that fails once Python · config_session.py
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

  1. Question 1 of 2 lazy val rates = { println("loading"); List(5, 12, 18) } is defined, and then the program uses rates.sum, rates.max and rates.min. How many times is "loading" printed?

    Type a number.

    A hint

    The first use runs the body. What do the later uses read?

    Show the answer: Question 1 of 2

    Answer: 1 times

    Defining a lazy val runs nothing. The first use, rates.sum, runs the body once and keeps the list; rates.max and rates.min read the kept list, so loading is printed exactly once.

  2. Question 2 of 2 A lazy val's right-hand side throws an exception the first time it runs, and the program catches it. What happens on the next use of the lazy val?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: The right-hand side runs again

    When the right-hand side throws, the lazy val stays uninitialised, and the next use runs the right-hand side again. In the lesson's program the first use of port failed and the second returned 8080.

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:

Inferred types, read from the REPL's answers Python · inference_session.py
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

  1. Question 1 of 2 What type does the compiler infer for val prices = List(40, 25.5)?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: List[Double]

    The REPL answered val prices: List[Double] = List(40.0, 25.5). An Int literal is adapted to the numeric type the other elements need; an Int value is not, so List(seats, 25.5) is a List[Int | Double].

  2. Question 2 of 2 var best = None compiles, but a later line best = Some(42) is refused. Which change fixes it?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: Write var best: Option[Int] = None

    Inference reads only the right-hand side, None, so best gets the type None.type and nothing else fits. Writing the type, Option[Int], makes room for the Some that comes later.

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

  1. Question 1 of 2 val fromForm = true. What type does the compiler infer for val id = if fromForm then "A-17" else 17?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: String | Int

    The two branches are a String and an Int with no common parent but root types, so the REPL answers val id: String | Int. A union of a number and a word is usually a sign of a mistake.

  2. Question 2 of 2 object Limits { final val MaxSeats = 180 } and val seats = 180 are defined. Which line compiles?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: val limit: 180 = Limits.MaxSeats

    180 as a type has exactly one value, 180. A final val in an object keeps that literal type, a plain val widens it to Int, and final is not allowed on local definitions.

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:

recursive/LateFee.scala Scala · recursive/LateFee.scala
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:

api-inferred/Prices.scala Scala · api-inferred/Prices.scala
object Prices:
  def discount(code: String) =
    if code == "STUDENT" then 10
    else if code == "STAFF" then 15
    else "none"
api-inferred/Checkout.scala Scala · api-inferred/Checkout.scala
@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:

The same mistake, with the result type inferred and declared Python · api_session.py
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:

Interactive · step by step Where the compiler reports a mistake in Prices.discount Step through the two compiles of api_session.py and see which file each error names.
Where the compiler reports a mistake in Prices.discount, step 5 of 5. Showing Prices.scala, Checkout.scala, result type (declared: Int), the error (E007 in Prices.scala:5). In focus: Prices.scala, the error.callsPrices.scaladef discount(code)Checkout.scalauses the discountresult typedeclared: Intthe errorE007 in Prices.scala:5

The drawing shows the whole scene; its 5 steps are listed below.

Every step as text
  1. The mistake. Someone added a branch to discount that returns the text "none" where every other branch returns a number of per cent.
  2. Inferred type. With no result type written, the compiler infers one from the branches, Int | String, and Prices.scala compiles without a word.
  3. 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.
  4. 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.
  5. 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

  1. Question 1 of 2 def lateFee(days: Int) = if days <= 0 then 0 else 50 + lateFee(days - 1) fails with E044. What fixes it?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: Writing its result type: def lateFee(days: Int): Int =

    E044 says Overloaded or recursive method lateFee needs return type. Inference cannot work out a type that depends on itself, so write : Int after the parameter list.

  2. Question 2 of 2 Prices.discount has a branch that returns "none" where the others return an Int, and Checkout.scala multiplies by its result. Where is the error reported once discount declares : Int?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: In Prices.scala, at the branch that returns "none"

    An inferred result type follows the body, so the mistake surfaced in the caller as E134. A declared result type is checked against the body, so the error moves to the line that is wrong.

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:

settings/Settings.scala Scala · settings/Settings.scala
// 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)
The practice program as given Python · settings_session.py
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.

  1. Question 1 of 12 Which keyword does the Scala 3 Book recommend for a new name, unless there is a reason to change it later?

    Choose one answer.

    Show the answer: Question 1 of 12

    Answer: val

    Use val by default and var only for a name that must be pointed at a new value. A val makes the code easier to follow, because the name means the same thing everywhere.

  2. Question 2 of 12 What does this print?

    Read the code, then choose one answer.

    val prices = List(40, 25)
    prices :+ 60
    println(prices)
    Show the answer: Question 2 of 12

    Answer: List(40, 25)

    prices :+ 60 returns a new list and leaves prices as it was. To keep the result, name it: val more = prices :+ 60, as Bindings.scala does.

  3. Question 3 of 12 A method must return the current time each time it is asked, and an object holds a 5 MB lookup table that most runs never use. Which keywords fit?

    Choose one answer.

    Show the answer: Question 3 of 12

    Answer: def for the time, lazy val for the table

    Choose by when the right-hand side must run: on every use (def), once on the first use (lazy val) or once where it is defined (val).

  4. Question 4 of 12 Eight threads read the same lazy val, without @threadUnsafe, at the same moment, and its right-hand side takes 50 ms. How often does the right-hand side run?

    Choose one answer.

    Show the answer: Question 4 of 12

    Answer: Once; the other threads wait and read its result

    Lazy val initialisation is thread-safe by default: in the lesson's program eight threads asked for the table and it was read 1 time. @threadUnsafe gives up that guard for speed.

  5. Question 5 of 12 def stamp = { println("computing"); 360 } is defined. How many lines does stamp + stamp + stamp print?

    Type a number.

    A hint

    A def keeps nothing between uses.

    Show the answer: Question 5 of 12

    Answer: 3 lines

    Each use of a def runs its body, so three uses print computing three times, and the sum is 1080. In the REPL session, stamp + stamp printed its line twice.

  6. Question 6 of 12 val seats = 180. What type does val share = seats / 7.0 get?

    Choose one answer.

    Show the answer: Question 6 of 12

    Answer: Double

    The compiler infers the type from the right-hand side: Int / Double is a Double, so share is a Double.

  7. Question 7 of 12 What type does the compiler infer for val x = if fromForm then Some("a") else None?

    Choose one answer.

    Show the answer: Question 7 of 12

    Answer: Option[String]

    When the branches share a parent that is not a root type, the compiler infers that parent: here Option[String], as offer was Option[Int] in the session.

  8. Question 8 of 12 val seats = 180. Why is List(40, 25.5) a List[Double] while List(seats, 25.5) is a List[Int | Double]?

    Choose one answer.

    Show the answer: Question 8 of 12

    Answer: Scala adapts an Int literal such as 40 to Double, but not an Int value such as seats

    Scala 3 adapts Int literals to other numeric types when needed, so 40 became 40.0. An Int value is not adapted, so the REPL answered val mixed: List[Int | Double] = List(180, 25.5).

  9. Question 9 of 12 What does the type Int | String mean?

    Choose one answer.

    Show the answer: Question 9 of 12

    Answer: A value that is either an Int or a String

    A | B is the union of A and B: each value is one of them. Code that uses such a value has to check which one it holds, for example with a match.

  10. Question 10 of 12 val seats = 180. Why is val other: 180 = seats refused with Required: (180 : Int)?

    Choose one answer.

    Show the answer: Question 10 of 12

    Answer: A plain val widens the literal type 180 to Int, and an Int is not known to be 180

    The type 180 has exactly one value. A plain val's type is widened to Int, so the compiler cannot promise that seats is 180, and it refuses the line.

  11. Question 11 of 12 Why does the Scala 3 Book recommend a written return type on every publicly visible method?

    Choose one answer.

    Show the answer: Question 11 of 12

    Answer: The type is the contract callers rely on, and with it a wrong body fails at the method instead of in the callers

    A written result type fixes the method's contract. The body is checked against it, so a mistake like else "none" is reported in Prices.scala, not as E134 in every caller.

  12. Question 12 of 12 var total = 0 and then total += 5_000_000_000L is refused with value += is not a member of Int. What is the cause, and the fix?

    Choose one answer.

    Show the answer: Question 12 of 12

    Answer: total was inferred as an Int from 0, so a Long cannot be added into it; write var total: Long = 0

    The first value fixes a var's type, as None did for best. 0 is an Int, and a Long sum does not fit an Int, so write the type: var total: Long = 0 (or start from 0L).

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 val fixes its name to one value and the compiler refuses a reassignment with E052, but the object it points to can still change; a var can be pointed at a new value.
  • A val runs its right-hand side where it is defined, a lazy val on its first use, once, even with several threads, and again after a failure, and a def on every use.
  • The compiler infers each type from the right-hand side alone, so var best = None gets None.type and needs a written type.
  • When branches have no common parent but Any, Scala 3 infers a union such as Int | String, and a final val in an object keeps a literal type such as 180.
  • 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

Report a problem with this lesson

Quick answers and tool search

Type to search tools or to get a quick answer, for example 18% of 2500. Use the up and down arrow keys to move through the results, Enter to choose, and Escape to close.