Methods & Parameters

This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production.

The reference material consulted while preparing these pages is listed in the section bibliography.

This page is written against Scala 3.10 (3.10.0, 2026-10-12) and every example was run with Scala CLI and Scala 3.10.0 on JDK 26 on 2026-10-15 (the examples were not re-run on 3.9 or 3.3). None of the features used is newer than 3.4, so they are available on the 3.9 LTS and 3.3 LTS lines as well; only the wording of some diagnostics and the x: _* deprecation (3.4) depend on the compiler version. A method is a def: a named, reusable block of code that belongs to a class, an object, a trait or the top level of a file. Methods are the unit of behaviour in Scala, and almost everything that looks like syntax in other languages (operators, indexing with (), + on numbers) is an ordinary method call. This page covers how a method is declared and called; the related topics have their own pages: Currying & Parameter Lists for several parameter lists, Laziness for lazy val and by-need evaluation, Functions as Values for the difference between a method and a function value, Extension Methods for adding methods to existing types, and Inline for methods expanded at compile time.

The examples use the trailhead scenario of this section: a toolkit that plans hikes on a network of mountain trails.

Defining methods

A method is introduced by def, followed by the name, the parameter list with a type for every parameter, an optional result type, = and the body. The body is an expression, and the value of its last expression is the result; there is no need for return.

//> using scala 3.10.0

def walkingMinutes(km: Double, ascentM: Int): Int =
  // Naismith's rule: 12 minutes per km plus 10 minutes per 100 m of ascent
  math.round(km * 12 + ascentM / 100.0 * 10).toInt

def describe(name: String) = s"Trail $name"       // return type inferred: String

def logStart(): Unit = println("planning...")

def sign(n: Int): String =
  if n > 0 then "up"
  else if n < 0 then "down"
  else "flat"

@main def e1(): Unit =
  logStart()
  println(walkingMinutes(7.5, 640))
  println(describe("Aguja"))
  println(sign(-3))
  val f: Int => String = sign        // eta-expansion: method becomes a function value
  println(f(5))
planning...
154
Trail Aguja
down
up

The rules worth knowing:

  • Parameter types are mandatory. Scala infers a method’s result type from its body but never the types of its parameters. The result type is optional for non-recursive methods, but writing it is the convention for every public method, because it keeps the inferred type from changing silently when the body changes.

  • A recursive method must declare its result type, as must an overloaded one; see the pitfalls section for the error.

  • Parameters are immutable. Assigning to one is a compile error, unlike in Java, where parameters are ordinary local variables.

  • Top-level methods (as above) need no enclosing object since Scala 3.0; they are compiled into a synthetic class named after the file. See Packages, Imports & Top-Level Definitions.

  • Unit is the "no useful result" type, the Scala counterpart of Java’s void. A Unit method exists for its side effects.

  • A method is not a value. Using a method where a function is expected converts it to a function value automatically (eta-expansion, as val f: Int => String = sign shows). Scala 3 also does it when no function type is expected: val g = sign is a function value of type Int => String (verified on 3.10.0), where Scala 2 required sign _.

  • def is evaluated at every use, val once, lazy val once on first use. This is the difference to keep in mind when you choose between them; the next example makes it visible.

//> using scala 3.10.0

var calls = 0
def tick(): Int =
  calls += 1
  calls

@main def e23(): Unit =
  val v = tick()               // evaluated once, now
  def d = tick()               // evaluated at every use
  lazy val l = tick()          // evaluated once, at first use
  println(s"v=$v d=$d d=$d l=$l l=$l")
  val f: () => Int = () => tick()  // an explicit function value, needs ()
  println(f())
v=1 d=2 d=3 l=4 l=4
5

Empty parameter lists and uniform access

A method can be declared with no parameter list (def current: Int) or with an empty one (def next(): Int). The two are different, and Scala 3 enforces the difference at every call:

  • declare def name: T for a pure accessor, something a client could not tell from a field (the uniform access principle), and call it as name;

  • declare def name(): T for a method with a side effect and call it as name().

//> using scala 3.10.0

class Counter:
  private var n = 0
  def next(): Int =
    n += 1
    n
  def current: Int = n

@main def e15(): Unit =
  val c = Counter()
  println(c.next)
  println(c.current())
  println("abc".length())     // Java-defined: both forms are accepted
  println("abc".length)
[error] ./e15.scala:12:11
[error] method next in class Counter must be called with () argument
[error]   println(c.next)
[error]           ^^^^^^
[error] ./e15.scala:13:11
[error] method current in class Counter does not take parameters
[error]   println(c.current())
[error]           ^^^^^^^^^
Compilation failed

Methods defined in Java (and the ones that override them, or that come from Scala 2 libraries) are exempt, which is why "abc".length and "abc".length() both compile: Java cannot make the distinction, so Scala does not insist on it for Java methods. Kotlin, by contrast, has properties for accessors and always requires () on a function call; Java always requires ().

Default arguments

A parameter can have a default value, written after the type. A caller may leave out any argument that has a default, and the compiler fills in the default expression at the call site. The default may refer to parameters of earlier parameter lists, but not to earlier parameters of the same list.

//> using scala 3.10.0

def pace(
    distanceKm: Double,
    ascentM: Int = 0,
    restMinutes: Int = 15,
    groupSize: Int = 1
): String =
  s"$distanceKm km, +$ascentM m, rest $restMinutes min, group of $groupSize"

@main def e2(): Unit =
  println(pace(8.0))
  println(pace(8.0, 400))
  println(pace(8.0, groupSize = 4))
  println(pace(groupSize = 2, distanceKm = 5.5, ascentM = 120))
  println(pace(10.0, 300, restMinutes = 30))
8.0 km, +0 m, rest 15 min, group of 1
8.0 km, +400 m, rest 15 min, group of 1
8.0 km, +0 m, rest 15 min, group of 4
5.5 km, +120 m, rest 15 min, group of 2
10.0 km, +300 m, rest 30 min, group of 1

How defaults work, and how they differ from other languages:

  • The compiler generates, for each default, a hidden method named name$default$N (pace$default$2 for the second parameter of pace). The call site invokes it; the default is therefore not baked into the callee’s bytecode, and a Java caller cannot omit arguments. Java has no default arguments at all and uses overloads instead; Kotlin has them and, as in Scala, they are not visible to Java callers unless you ask for overloads (@JvmOverloads).

  • Because the default is a method, an override may change it. The call below uses the default of the object’s runtime class, not the one written in the static type.

  • Only one overloaded alternative may define defaults (see the pitfalls section for the error).

//> using scala 3.10.0

class Planner:
  def plan(km: Double, extra: Int = 1): String = s"base $km $extra"

class FastPlanner extends Planner:
  override def plan(km: Double, extra: Int = 99): String = s"fast $km $extra"

@main def e5(): Unit =
  val p: Planner = FastPlanner()
  println(p.plan(2.0))
  println(FastPlanner().plan(2.0))
fast 2.0 99
fast 2.0 99

Named arguments

Any argument can be passed by parameter name, in any order. Named and positional arguments can be mixed, with one rule: a positional argument may follow a named one only when the named argument is in its own position. The compiler also rejects unknown names and a parameter given twice.

//> using scala 3.10.0

def pace(distanceKm: Double, ascentM: Int = 0, groupSize: Int = 1): String =
  s"$distanceKm/$ascentM/$groupSize"

@main def e6(): Unit =
  println(pace(distanceKm = 5.0, 300))   // named argument in its own position, then positional
5.0/300/1

The same call with the named argument out of position, and two other mistakes, give these real compiler messages (each snippet was compiled separately):

println(pace(ascentM = 100, 5.0))
println(pace(5.0, ascent = 100))
println(pace(5.0, ascentM = 1, ascentM = 2))
[error] positional after named argument
[error]   println(pace(ascentM = 100, 5.0))
[error]                               ^^^
[error] method pace: (distanceKm: Double, ascentM: Int, groupSize: Int): String does not have a parameter ascent
[error]   println(pace(5.0, ascent = 100))
[error]                     ^^^^^^^^^^^^
[error] parameter ascentM of method pace: (distanceKm: Double, ascentM: Int, groupSize: Int): String is already instantiated
[error]   println(pace(5.0, ascentM = 1, ascentM = 2))
[error]                                  ^^^^^^^^^^^

Named arguments make the parameter names part of a method’s public API: renaming a parameter breaks callers that name it. Named arguments work for Scala-defined methods only; a Java method’s parameter names are not available to Scala callers by default, so they cannot be named. Kotlin and Python also have named arguments; Java does not.

By-name parameters

A parameter whose type is written => T is by-name: the argument expression is not evaluated at the call. It is evaluated every time the method body uses the parameter, and not at all if the body never does. An ordinary parameter is by-value, evaluated once, before the method runs. By-name parameters are how Scala writes control structures as library methods, such as Option.getOrElse, Try, Future and the boundary helpers, and how a message is built only when it is needed.

//> using scala 3.10.0

var evaluations = 0

def forecast(): Boolean =
  evaluations += 1
  println("  (calling the weather service)")
  true

def byValue(clear: Boolean): String = if clear then "go" else "stay"
def byName(clear: => Boolean): String = if clear then "go" else "stay"
def twice(clear: => Boolean): Int = if clear && clear then 2 else 0

def when(open: Boolean)(msg: => String): Unit =
  if open then println(msg)

@main def e7(): Unit =
  println("by value:")
  println(byValue(forecast()))
  println("by name, used once:")
  println(byName(forecast()))
  println("by name, used twice:")
  println(twice(forecast()))
  println(s"evaluations = $evaluations")

  println("lazy message:")
  when(false)({ println("  (building the message)"); "never shown" })
  when(true)("hut is open")
by value:
  (calling the weather service)
go
by name, used once:
  (calling the weather service)
go
by name, used twice:
  (calling the weather service)
  (calling the weather service)
2
evaluations = 4
lazy message:
hut is open

Details:

  • A by-name parameter behaves like a parameterless function () => T that the compiler creates and calls for you. Unlike a by-need (lazy) value it re-evaluates on every use; if you want "at most once", copy it into a lazy val inside the method (see Laziness).

  • The argument is evaluated in the caller’s scope, so it can use the caller’s variables, and a side effect inside it happens as many times as the method uses it (twice for twice above).

  • By-name applies to parameters of methods and of classes (not of case class constructors or val/var class parameters, which must be values). There is also a by-name flavour for context parameters, using x: => T (Givens & Using Clauses).

  • A by-name parameter is erased to Function0, so two overloads that differ only in => A versus => B clash; see @targetName below. Java and Kotlin have no by-name parameters; they pass a Supplier or a () -> T lambda and the call site writes the lambda.

Repeated parameters and vararg splices

A parameter type followed by * is a repeated parameter (a vararg): the method accepts zero or more arguments of that type, and inside the body the parameter is a Seq[T] (an immutable Seq since Scala 2.13). Only the last parameter of a list may be repeated. To pass an existing collection as the varargs, splice it with a postfix * after the expression: xs*.

//> using scala 3.10.0

def totalKm(legs: Double*): Double = legs.sum

def label(prefix: String, names: String*): String =
  names.map(n => s"$prefix$n").mkString(", ")

@main def e8(): Unit =
  println(totalKm())
  println(totalKm(3.5))
  println(totalKm(3.5, 2.0, 4.25))
  val legs = List(1.0, 2.0, 3.0)
  println(totalKm(legs*))            // splice a Seq with xs*
  println(totalKm(0.5 +: legs*))     // splice an expression
  println(label("trail-", "A", "B"))
  val arr = Array("x", "y")
  println(label("p-", arr*))
0.0
3.5
9.75
6.0
6.5
trail-A, trail-B
p-x, p-y

Splicing changed syntax in Scala 3. Scala 2 wrote xs: _*; Scala 3 writes xs*, "analogously to how a vararg parameter is declared". The old form is deprecated since Scala 3.4 and on 3.10.0 still compiles but warns, and the compiler can rewrite it (the rewrite changed the source file shown below, the -source:3.4-migration -rewrite options are in the first two lines):

//> using scala 3.10.0
//> using option -source:3.4-migration -rewrite

def totalKm(legs: Double*): Double = legs.sum

@main def e21(): Unit =
  val legs = List(1.0, 2.0)
  println(totalKm(legs: _*))
[warn] ./e21.scala:8:25
[warn] The syntax `x: _*` is no longer supported for vararg splices; use `x*` instead
[warn] This construct can be rewritten automatically under -rewrite -source 3.4-migration.
[warn]   println(totalKm(legs: _*))
[warn]                         ^
[patched file e21.scala]
3.0

After the rewrite the call line reads println(totalKm(legs*)). The same * is used in patterns (case List(0, 1, xs*)); see Pattern Matching.

Two limits apply when you splice. A sequence cannot be passed where a single element is expected, and a splice must be the only argument for the repeated parameter, on 3.10.0 without any flags:

println(totalKm(legs))
println(totalKm(1.0, legs*))
[error] Found:    (legs : List[Double])
[error] Required: Double
[error]   println(totalKm(legs))
[error]                   ^^^^
[error] Sequence argument type annotation `*` cannot be used here:
[error] it is not the only argument to be passed to the corresponding repeated parameter Double*
[error]   println(totalKm(1.0, legs*))
[error]                        ^^^^^

Mixing elements and a splice (totalKm(1.0, legs*)) is what the flexible varargs proposal (SIP-70) addresses; it was experimental on 3.9 and is not covered here, so use 1.0 +: legs* as in the example. Java sees a repeated parameter as a Seq, not an array. Java’s T... is an array, and Kotlin’s vararg is an array with the spread operator *arr.

Nested methods

A method can be defined inside another method, or inside any block. A nested method sees the enclosing method’s parameters and local values (it captures them), but nothing outside sees it. This is the usual way to hide a helper, to write an accumulating loop, and to avoid giving a one-off function a place in the public API.

//> using scala 3.10.0

def climb(ascents: List[Int]): Int =
  def sum(rest: List[Int], acc: Int): Int =        // nested helper
    rest match
      case Nil => acc
      case h :: t => sum(t, acc + h)
  sum(ascents, 0)

def scaled(xs: List[Int], factor: Int): List[Int] =
  def times(n: Int) = n * factor                   // captures factor from the enclosing method
  xs.map(times)

@main def e11(): Unit =
  println(climb(List(120, 340, 80)))
  println(scaled(List(1, 2, 3), 10))
540
List(10, 20, 30)

Nested methods can be recursive (and tail-recursive, see Recursion & Tail Calls) and, like any recursive method, need an explicit result type. Java has no nested methods (only lambdas and local classes); Kotlin has local functions much like Scala’s.

Operators are methods

Scala has no operators in the language sense. a + b is the method call a.+(b), and any method with a single parameter can be written infix, a method b. The method names +, ::, <<, && and so on are ordinary identifiers built from symbolic characters, which means you can define your own the same way. The compiler turns the symbolic names into alphanumeric names in the bytecode (+ becomes $plus).

//> using scala 3.10.0
import scala.annotation.targetName

case class Km(value: Double):
  @targetName("plus")
  def +(other: Km): Km = Km(value + other.value)
  @targetName("times")
  def *(factor: Double): Km = Km(value * factor)
  def unary_- : Km = Km(-value)
  infix def per(days: Int): Double = value / days
  def max(other: Km): Km = if value >= other.value then this else other

@main def e12(): Unit =
  val a = Km(4.0)
  val b = Km(2.5)
  println(a + b)
  println(a.+(b))
  println(a * 2)
  println(-a)
  println(a per 2)                 // allowed: declared infix
  println(a.max(b))
  println(a max b)                 // alphanumeric infix without the modifier
[warn] ./e12.scala:22:13
[warn] Alphanumeric method max is not declared infix; it should not be used as infix operator.
[warn] Instead, use method syntax .max(...) or backticked identifier `max`.
[warn] The latter can be rewritten automatically under -rewrite -source 3.4-migration.
[warn]   println(a max b)                 // alphanumeric infix without the modifier
[warn]             ^^^
Km(6.5)
Km(6.5)
Km(8.0)
Km(-4.0)
2.0
Km(4.0)
Km(4.0)

The pieces, from the example:

  • Infix, prefix and postfix. a + b is infix. A prefix operator is a method named unary_+, unary_-, unary_! or unary_~ (only those four), called as -a; note the space before the colon in def unary_- : Km, which stops the : from becoming part of the name. Postfix calls (a max) are a feature that needs an import and a warning and are best avoided.

  • Alphanumeric methods used infix (a max b) need the infix modifier on their definition; without it the 3.10.0 compiler warns, as shown. See below.

  • Backticks make any text an identifier: trail length in km is a legal method name, and a backticked name can also be used to call a reserved word defined elsewhere. Operators must be defined with a name that is legal on the JVM or have a @targetName.

Precedence and associativity

When an expression chains operators, the first character of each operator decides its precedence. From the lowest to the highest:

Precedence (lowest first) First character of the operator

1

any letter, $, _

2

|

3

^

4

&

5

= or !

6

< or >

7

:

8

+ or -

9

*, / or %

10

any other special character

Parentheses override it. The last character decides associativity: an operator ending in : is right-associative (a :: b :: c is a :: (b :: c), and x :: xs is the method call xs.::(x) on the right operand), and every other operator is left-associative. Also, an operator ending in = that is not a comparison (+=, ++=, ::=) is an assignment operator: if the left side has no method with that name, a += b is rewritten to a = a + b and a must be a var.

//> using scala 3.10.0

@main def e13(): Unit =
  println(1 + 2 * 3)
  println((1 + 2) * 3)
  println(2 :: 3 :: Nil)              // right-associative: ends with ':'
  println(Nil.::(3).::(2))            // what the compiler turns it into
  println(10 - 3 - 2)                 // left-associative
  println(1 << 2 + 1)                 // + binds tighter than <<
  println(1 + 1 == 2 && 3 > 2 || false)
  val xs = List(1, 2, 3)
  println(0 +: xs :+ 4)               // mixing prepend and append
7
9
List(2, 3)
List(2, 3)
5
8
true
List(0, 1, 2, 3, 4)

You can define a right-associative operator and an assignment-style use the same way. Here a Route grows from either end:

//> using scala 3.10.0
import scala.annotation.targetName

case class Route(stops: List[String]):
  @targetName("prepend")
  def ::(stop: String): Route = Route(stop :: stops)      // right-associative: ends with ':'
  @targetName("append")
  def +(stop: String): Route = Route(stops :+ stop)

def `trail length in km`(): Double = 12.5

@main def e22(): Unit =
  val r = "Refugio" :: "Collado" :: Route(List("Pico"))
  println(r)
  var route = r
  route += "Valle"                                         // expands to route = route + "Valle"
  println(route)
  println(`trail length in km`())
Route(List(Refugio, Collado, Pico))
Route(List(Refugio, Collado, Pico, Valle))
12.5

Java and Kotlin differ here. Java has a fixed set of operators with fixed precedence and no user-defined ones (not even + on your own types); Kotlin allows overloading a fixed set of operators through operator fun plus(...) and infix fun for named ones. In Scala you choose the name, and its first and last character decide the precedence and associativity, so keep your own operators few and conventional.

The infix modifier

Scala 3 lets the author of a method decide whether it may be written infix, so every use site reads the same way. An infix method must take exactly one parameter in its first non-receiver parameter list. The modifier is also allowed on types with exactly two type parameters (infix type or[X, Y], usable as String or Int). Rules, from the reference:

  • Symbolic operators are always allowed infix, so infix is redundant on them.

  • Alphanumeric methods used infix are deprecated unless the method carries infix, was compiled with Scala 2, or the call is followed by an opening brace. The deprecation starts from Scala 3.1; on 3.10.0 it is the warning shown in the a max b example, with a rewrite to the backticked form a max b available under -rewrite -source 3.4-migration.

  • infix is a soft modifier: it is only a keyword in modifier position, so infix remains a legal identifier.

  • An override must agree: if a method overrides another, both are infix or neither is.

Libraries use infix to keep a fluent style legal, as in the a per 2 call above and in the route DSL covered in Domain Specific Languages. Kotlin’s infix fun is the same idea; Scala 2 had no such modifier and allowed any one-parameter method infix.

@targetName

@targetName("external") (package scala.annotation) gives a definition a different name in the generated bytecode. The Scala name is unchanged, and Scala callers keep writing the symbolic name; the external name is what Java sees, what stack traces print, and what disambiguates definitions that would clash after erasure. The reference recommends it for every symbolic operator, and a definition with a backticked name that is not legal on the platform also needs one. It is allowed on every kind of definition except a top-level class, trait or object, and the name must be legal on the host platform. javap shows the effect:

//> using scala 3.10.0
import scala.annotation.targetName

object Ops:
  def plain(x: Int): Int = x
  def +(x: Int, y: Int): Int = x + y
  @targetName("add")
  def ++(x: Int, y: Int): Int = x + y
  def pace(km: Double, ascent: Int = 0): String = s"$km/$ascent"
public final class Ops$ {
  public static final Ops$ MODULE$;
  public static {};
  public int plain(int);
  public int $plus(int, int);
  public int add(int, int);
  public java.lang.String pace(double, int);
  public int pace$default$2();
}

The listing is the output of javap -cp <classes> 'Ops$' after compiling the object with Scala 3.10.0 (it also shows the pace$default$2 method that carries the default argument). For a top-level object, Scala also emits static forwarders, so a Java caller writes Ops.add(1, 2) for ++ instead of the cryptic Ops.$plus$plus(1, 2), while + (with no annotation) keeps its encoded name Ops.$plus. @targetName also resolves clashes: two overloads that differ only in a by-name parameter type both erase to Function0 and are rejected, and one annotation fixes it.

//> using scala 3.10.0
import scala.annotation.targetName

def f(x: => String): Int = x.length
def f(x: => Int): Int = x + 1
[error] ./e24.scala:5:5
[error] Conflicting definitions:
[error] def f(x: => String): Int in the top-level definitions in package <empty> at line 4 and
[error] def f(x: => Int): Int in the top-level definitions in package <empty> at line 5
[error] have the same type (x: Function0): Int after erasure.
[error]
[error] Consider adding a @targetName annotation to one of the conflicting definitions
[error] for disambiguation.
[error] def f(x: => Int): Int = x + 1
[error]     ^

With @targetName("fString") on the first definition the program compiles, and (f("abc"), f(41)) prints (3,42). An override must keep the target name of the method it overrides: adding or changing a @targetName on an override is an error, because the erased names must agree.

Dropped: procedure syntax and auto-application

Two Scala 2 idioms are gone.

Procedure syntax, a method body in braces without =, was a way to declare a Unit method. In Scala 3 it is a syntax error; write : Unit =. The compiler can rewrite old code, and Scalafix also has a rule for it.

//> using scala 3.10.0

def hello() {
  println("hello")
}

@main def e16(): Unit = hello()
[error] ./e16.scala:3:13
[error] '=' expected, but '{' found
[error] def hello() {
[error]             ^
Compilation failed

With //> using option -source:3.0-migration -rewrite the same file compiles, prints hello, and the compiler rewrites the declaration to def hello(): Unit = {.

Auto-application, calling next for def next(): T and having the compiler insert (), is an error in Scala 3 (see the Counter example above). The call must match the declaration exactly, and an override must agree with the overridden method on (), with the Java and Scala 2 exceptions described there. Existing code can be compiled with -source:3.0-migration, and -rewrite fixes the calls.

Putting it together

The planner below combines a symbolic operator with @targetName, an infix method, a repeated parameter spliced with xs*, a second parameter list, and named and default arguments. The assertion at the end checks the per result.

//> using scala 3.10.0
import scala.annotation.targetName

case class Km(value: Double):
  @targetName("plus")
  def +(other: Km): Km = Km(value + other.value)
  infix def per(days: Int): Km = Km(value / days)

def plan(legs: Km*)(name: String = "Untitled", restMinutes: Int = 15): String =
  val total = legs.foldLeft(Km(0))(_ + _)
  s"$name: ${total.value} km, rest $restMinutes min, ${legs.size} legs"

@main def e26(): Unit =
  val legs = List(Km(4.0), Km(2.5))
  println(plan(legs*)(name = "Aguja loop"))
  println(plan(Km(1.0))(restMinutes = 5))
  assert((Km(6.0) per 3) == Km(2.0))
Aguja loop: 6.5 km, rest 15 min, 2 legs
Untitled: 1.0 km, rest 5 min, 1 legs

Pitfalls

  • Recursive methods need a result type. Without it the compiler cannot infer the type of the recursive call.

  • Parameters cannot be reassigned. Copy to a local var if you must.

  • Only one overload may declare default arguments.

  • Positional after named is rejected unless the named argument sits in its own position.

The first three compile errors, from compiling the snippets below:

def bump(n: Int): Int =
  n = n + 1
  n

def fact(n: Int) = if n <= 1 then 1 else n * fact(n - 1)

class Trailhead:
  def plan(km: Double, rest: Int = 10): String = "a"
  def plan(name: String, rest: Int = 10): String = "b"
[error] Reassignment to val n
[error]   n = n + 1
[error]   ^^^^^^^^^
[error] Overloaded or recursive method fact needs return type
[error]
[error]  Run with -explain-cyclic for more details.
[error] def fact(n: Int) = if n <= 1 then 1 else n * fact(n - 1)
[error]                                              ^
[error] two or more overloaded variants of method plan have default arguments
[error]   def plan(name: String, rest: Int = 10): String = "b"
[error]       ^
  • -5.abs parses with a warning (Illegal literal) on 3.10.0; write (-5).abs or 5.abs for clarity.

  • Alphanumeric infix (a max b) warns unless the method is infix or was compiled with Scala 2.

  • Operators change precedence by their first character. A custom operator that begins with | binds looser than one beginning with +; check the table before relying on a chain.

  • Default values are evaluated at every call in which they are used, like a by-name parameter. A default such as startedAt: Long = System.currentTimeMillis reads the clock on each call.

Where the books are outdated

The books consulted for this section (see the section bibliography) were written for Scala 2 or for the first Scala 3 releases. For methods and parameters, treat these points as superseded by the pages cited here:

  • Procedure syntax and auto-application appear in Scala 2 code and in the older books. Both are errors in Scala 3.

  • xs: _* is how the older books splice a collection; Scala 3 uses xs* and warns about the old form on 3.10.0.

  • Infix use of alphanumeric methods is shown without a modifier; Scala 3 expects infix and warns otherwise.

  • @targetName and infix do not exist in the Scala 2 books at all.

  • By-name parameters, default and named arguments, nested methods and operator precedence are unchanged since Scala 2 and the explanations still hold; only the surrounding syntax (braceless blocks, top-level methods, then) differs.

The run results above are from 2026-10-15 on Scala 3.10.0 and were not re-run on the 3.9 LTS or 3.3 LTS releases.