Optional Braces & Indentation
|
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.0 (released 2026-10-12) and every example was run with Scala CLI 1.17.1 on JDK 26 (checked on 2026-10-15). Scala 3 lets a program drop most braces: a block is delimited by indentation, much as in Python, while braces keep working and can be mixed freely with indentation. The two styles produce the same program, so this is a choice of syntax and not a change of meaning. Every feature on this page except relaxed lambda syntax is stable on the 3.3 LTS and the 3.9 LTS lines as well: indentation syntax and the quiet control syntax arrived with Scala 3.0, and "fewer braces" (a colon in place of braces around a method argument) was experimental from 3.1 and became stable in 3.3. Relaxed lambda syntax is a preview feature in 3.10 (experimental in 3.9) and is covered in its own section at the end.
The examples follow the trailhead scenario used across this section, a toolkit that plans hikes on a network of mountain trails. The matching control-flow topics are on Conditionals & Loops, For Comprehensions and Pattern Matching; the formatting conventions that go with this syntax are on Style Guide.
The big picture
Three separate mechanisms are involved, and it helps to keep them apart.
| Mechanism | What it changes | Turned off or rewritten with |
|---|---|---|
Significant indentation |
|
|
Quiet control syntax |
parentheses around |
|
End markers |
an optional |
none (they are ordinary syntax) |
Significant indentation is on by default for every file compiled with Scala 3. The compiler inserts virtual <indent> and
<outdent> tokens at line breaks, and a pair of those behaves exactly like a pair of braces. The rule is therefore that adding
a pair of optional braces never changes the meaning of a well-indented program.
The indentation rules
Independently of whether braces are optional, the compiler flags badly indented programs as warnings. There are two rules:
-
In a brace-delimited region, no statement may start to the left of the first statement after the opening brace that starts a new line. This catches a missing closing brace.
-
When significant indentation is turned off (Scala 2 mode or
-no-indent) and an indented sub-part of an expression ends in a newline, the next statement must start at an indentation width less than the sub-part. This catches a missing opening brace.
//> using scala 3.10.0
@main def run(): Unit = {
val x = 1
if (x > 0) {
println(1)
println(2)
println("done")
}
}
[warn] ./w1.scala:6:5
[warn] Line is indented too far to the left, or a '}' is missing
[warn] println(2)
[warn] ^
[warn] ./w1.scala:7:3
[warn] Line is indented too far to the left, or a '}' is missing
[warn] println("done")
[warn] ^
1
2
done
The program still compiles and runs, because the braces are the authority here. The second rule has its own warning:
//> using scala 3.10.0
//> using option -no-indent
@main def run(): Unit = {
val x = -1
if (x < 0)
println(1)
println(2)
}
[warn] ./w2.scala:7:5
[warn] Line is indented too far to the right, or a '{' is missing before:
[warn]
[warn] println(1)
[warn] println(2)
[warn] ^
1
2
Both rules leave wide freedom. They say nothing about indentation inside an expression, and they do not require every statement of a block to line up exactly.
Where an indentation region can start
An <indent> is inserted at a line break when two things are both true: an indentation region can start at the current
position, and the first token on the next line is indented strictly more than the current indentation width. A region can
start:
-
after one of the tokens =,
=>,?=>,<-,catch,do,else,finally,for,if,match,return,then,throw,try,whileandyield; -
after the closing parenthesis of the condition of an old-style
iforwhile, and after the closing parenthesis or brace of the enumerators of an old-styleforwithoutdo; -
after the leading parameters of an
extension, and afterwithin a given instance; -
after a colon that starts a template body or that stands where a method argument is expected (the next two sections).
An <outdent> is inserted when the first token on the next line is indented strictly less than the current width, unless the
previous line ends with a token that shows the statement continues (then, else, do, catch, finally, yield,
match). Several outdents can be inserted in a row. After an outdent the new line must match the indentation of some earlier
line of the enclosing region. Indentation tokens are inserted only where the compiler also infers newline separators: at the
top level and inside braces, but not inside parentheses, patterns or types.
Colon for template bodies
The body of a class, trait, object, enum, given instance, type refinement or package can be written as a colon at the end of
the header followed by indented definitions. The colon is the ordinary colon; the compiler reads it as a template-body
opener only where the grammar allows one and only after an identifier, a backticked identifier, this, super, ) or ].
//> using scala 3.10.0
package trailhead:
enum Difficulty:
case Easy, Hard
trait Rated:
def rating: Int
class Trail(val name: String, val km: Double) extends Rated:
def rating: Int = if km > 10 then 3 else 1
object Trail:
def apply(name: String, km: Double): Trail = new Trail(name, km)
given Ordering[Trail] = Ordering.by(_.km)
extension (t: Trail)
def label: String = s"${t.name} (${t.km} km)"
@main def run(): Unit =
import trailhead.{*, given}
val anon = new Rated:
def rating = 7
val t = Trail("Pico", 12.5)
println(t.label)
println(anon.rating)
println(t.rating)
val sorted = List(Trail("A", 9), Trail("B", 2)).sorted
println(sorted.map(_.name))
Pico (12.5 km)
7
3
List(B, A)
A few details are worth remembering:
-
In each case the trailing
:can be replaced, without change of meaning, by braces around the indented definitions. -
A class or object with no members needs no colon at all:
class Marker,object Empty. -
new Rated:followed by an indented body creates an anonymous class. In Scala 2 the same code needsnew Rated { ... }. -
The old
given Show[Trail] withform still works on 3.10 (a trailingwithalso opens a region), but the preferred form is the colon. Under-source:futurethe compiler rejects it with the message "Given member definitions starting withwithare no longer supported; use{...}or:followed by newline instead" (the form is still accepted by default on 3.10.0). Anonymous givens are covered on Givens & Using Clauses. -
The members of the package are indented only when the package clause uses a colon. The common file-level form
package trailheadon its own line (without a colon) keeps all the file’s definitions at column 0.
Quiet control syntax: then and do
The "quiet" syntax removes parentheses around conditions and generators. Where a keyword would otherwise be followed by a
parenthesised condition, Scala 3 accepts a bare condition if it is followed by then (for if) or do (for while and
for). A for that ends in yield produces a value, and one that ends in do is a loop.
//> using scala 3.10.0
import scala.util.control.NonFatal
@main def run(): Unit =
val kms = List(12.5, 4.0, 8.0)
// if / then / else
val km = kms.head
val kind =
if km > 10 then "long"
else if km > 5 then "medium"
else "short"
println(kind)
// while / do
var left = 3
while left > 0 do left -= 1
println(left)
// for ... do, for ... yield, guards
for k <- kms if k > 5 do println(k)
val doubled = for k <- kms yield k * 2
println(doubled)
// multi-line generators
for
k <- kms
m <- List(1, 2)
if k > 5
do
println(s"$k x $m")
// try / catch with one case on the same line, then several in an indented block
val n = try "abc".toInt catch case _: NumberFormatException => -1
println(n)
val m =
try Integer.parseInt("x1")
catch
case _: NumberFormatException => -2
case NonFatal(e) => -3
finally println("parsed")
println(m)
// old syntax still compiles
if (km > 10) println("old style")
for (k <- kms) { if (k > 100) println(k) }
long
0
12.5
8.0
List(25.0, 8.0, 16.0)
12.5 x 1
12.5 x 2
8.0 x 1
8.0 x 2
-1
parsed
-2
old style
The rules in detail:
-
The condition of an
ifcan be written without parentheses if it is followed bythen. -
The condition of a
whilecan be written without parentheses if it is followed bydo. -
The generators of a
forcan be written without parentheses or braces if they are followed byyieldordo. -
A
doafter the generators means a loop. In Scala 2 the loop form wasfor (x <- xs) bodyand the value form wasfor (x <- xs) yield body. -
A
catchcan be followed by a singlecaseon the same line. With several cases they go in braces (as in Scala 2) or in an indented block.
Leaving out then on a braceless condition is an error, and the message is not obvious, because the compiler first reads
println("positive") as the rest of the condition:
//> using scala 3.10.0
@main def run(): Unit =
val x = 1
if x > 0 println("positive")
[error] ./e3.scala:5:1
[error] 'then' expected, but eof found
[error] ./e3.scala:4:6
[error] value println is not a member of Boolean
[error] if x > 0 println("positive")
[error] ^^^^^^^^^^^^^
The parenthesised forms if (x > 0) ... and for (k <- ks) ... remain legal in Scala 3 unless you compile with -new-syntax.
Fewer braces: a colon instead of braces around an argument
Since Scala 3.3 (experimental in 3.1 and 3.2), a colon at the end of a line is also recognised where a function argument is
expected. The indented block that follows becomes the last argument of the call. This is what makes control-like methods read
as built-in syntax: repeat(2):, Using.resource(...): r =>, xs.map: x =>. The colon can be followed on the same line by
the parameter part and the arrow of a lambda.
//> using scala 3.10.0
import scala.util.Using
case class Trail(name: String, km: Double)
def timed[T](label: String)(body: => T): T =
val result = body
println(s"$label done")
result
def repeat(n: Int)(body: => Unit): Unit =
for _ <- 1 to n do body
@main def run(): Unit =
val trails = List(Trail("Pico", 12.5), Trail("Lago", 4.0), Trail("Cumbre", 8.0))
// a by-name block after ':'
repeat(2):
println("ah")
println("ha")
// a lambda, parameter on the same line as the colon
val hours = trails.map: t =>
val h = t.km / 4
h + 0.5
println(hours)
// the lambda parameter on its own, and a lambda with two parameters
val total = trails.foldLeft(0.0): (acc, t) =>
acc + t.km
println(total)
// partial function: a block of cases after ':'
val labels = trails.map:
case Trail(n, km) if km > 10 => s"$n: long"
case Trail(n, _) => s"$n: short"
println(labels)
// chaining: a leading '.' may sit at an odd indentation
val names = trails
.filter: t =>
t.km > 5
.map: t =>
t.name.toUpperCase
println(names)
// method with a second parameter list
val r = timed("plan"):
trails.map(_.km).sum
println(r)
// ': ' after an infix operator
val extra = List(1) ++:
List(2, 3)
println(extra)
// resource handling
val out = Using.resource(new java.io.StringReader("abc")): reader =>
reader.read().toChar
println(out)
ah
ha
ah
ha
List(3.625, 1.5, 2.5)
24.5
List(Pico: long, Lago: short, Cumbre: short)
List(PICO, CUMBRE)
plan done
24.5
List(1, 2, 3)
a
What the example shows:
-
repeat(2):passes the indented block as the by-name argument of the last parameter list. A method such astimed("plan"):with a second parameter list works the same way: the colon applies to the last argument. -
trails.map: t =>followed by an indented body is a lambda with one parameter. The same shape works forfoldLeft(0.0): (acc, t) =>and forUsing.resource(...): reader =>. -
A colon followed directly by
caseclauses on the next lines is a partial function (a match without a scrutinee). -
In a chain, a line that starts with
.is accepted even when its indentation does not match any earlier line, as long as it differs by at least two spaces from both neighbouring regions. -
A colon can also follow an infix operator written in backticks, as in the reference’s
credentialsexample. This was run separately (it is not in the listing above):
//> using scala 3.10.0
@main def run(): Unit =
val extra = List(1) `++`:
List(2, 3)
println(extra)
List(1, 2, 3)
The trailing : of a right-associative operator such as ++: is part of the operator name, so there the colon does
not open an argument.
-
A call with parenthesised arguments can end with a colon argument as well: the
foldLeft(example above passes"start"in parentheses on its own line and the lambda after): (acc, x) =>.
The block is always the last argument. A single lambda is the common case, but any parameter, including a by-name one, can be
supplied this way. The method itself is unchanged: timed and repeat are ordinary Scala 2-style definitions, and the same
code can be written with braces.
The colon argument always ends the call expression, so it can only supply the last argument. It is not confused with a type
ascription such as val x: Int, because there the colon is followed by a type on the same line and no indented block.
Mixing braces and indentation
Indentation can be mixed freely with braces { }, brackets [ ] and parentheses ( ). For interpreting indentation in such
regions:
-
the assumed indentation width of a multiline region in braces is the indentation of the first token that starts a new line after the opening brace;
-
for a multiline region in brackets or parentheses, it is the indentation of the token following the opening bracket or parenthesis if that is at the end of a line, and the indentation of the enclosing region otherwise;
-
on a closing brace, bracket or parenthesis, as many outdents as necessary are inserted to close all open nested regions.
//> using scala 3.10.0
class Planner { // braces still work
def plan(kms: List[Double]): Double =
kms.map: km => // indentation inside braces
km * 1.25
.sum
}
def grade(km: Double): String = km match
case k if k > 10 => "long" // cases at the same indent as 'match'
case k if k > 5 => "medium"
case _ => "short"
@main def run(): Unit =
println(Planner().plan(List(1.0, 2.0)))
println(grade(12))
// a leading infix operator continues the previous line
val total = 1
+ 2
+ 3
println(total)
// a call with its arguments on a more-indented line starting with '('
def add(a: Int)(b: Int) = a + b
val z = add(1)
(2)
println(z)
// fewer braces inside parentheses
val s = List(1, 2, 3).foldLeft(
"start"
): (acc, x) =>
acc + x
println(s)
3.75
long
6
3
start123
The example uses three further rules from the reference:
-
Case clauses. A
match(orcatch) may be followed bycaseclauses at the same indentation as thematchitself. The region ends at the first token at that width that is not acase. Most code indents the cases one level; the unindented form is accepted for compatibility. -
Leading infix operators. A line that starts with an infix operator continues the previous expression, so
1,+ 2,+ 3on three lines is6(the example prints6). -
A dot at an odd indent (shown above) is accepted for method chains.
Indentation and statement continuation
Indentation also decides whether two consecutive lines are one statement. A virtual semicolon is not inserted if the second
line is indented more than the first and either starts with (, [ or { or the first line ends with return. In Scala 2 a
line starting with { always continues the call on the previous line, and that behaviour is kept under -no-indent.
//> using scala 3.10.0
def add(a: Int)(b: Int): Int = a + b
@main def run(): Unit =
val more =
add(1)
(2)
println(more)
3
If the second line were aligned with the first ((2) at the same column as add(1)), it would be a separate statement and
the compiler would reject the first line:
[error] ./w4.scala:6:5
[error] missing argument list for value of type Int => Int
[error] add(1)
[error] ^^^^^^
Tabs and spaces
Indentation prefixes can be spaces, tabs or a mix. Indentation widths are the prefixes themselves, ordered by the string prefix relation: "2 tabs followed by 4 spaces" is strictly less than "2 tabs followed by 5 spaces", but incomparable to "6 tabs" or to "4 spaces followed by 2 tabs". It is an error if the indentation width of a line is incomparable with the width of the current region. Mixing a tab-indented line into a file whose enclosing lines use spaces gives:
[error] ./t1.scala:4:2
[error] Incompatible combinations of tabs and spaces in indentation prefixes.
[error] Previous indent : 2 spaces
[error] Latest indent : 1 tab
[error] val y = x + 1
[error] ^
A file that uses only tabs compiles and runs. The official style guide uses two spaces and no tabs, which avoids this whole class of error.
A line whose width sits between two earlier widths is a separate, common error. Here the body of f is indented four
spaces further than def f, and the result line is indented two:
//> using scala 3.10.0
@main def run(): Unit =
def f(x: Int) =
val y = x + 1
y * 2
println(f(1))
[error] ./e6.scala:5:5
[error] The start of this line does not match any of the previous indentation widths.
[error] Indentation width of current line : 4 spaces
[error] This falls between previous widths: 2 spaces and 6 spaces
[error] y * 2
[error] ^
The same message appears when else is indented one space further than the if it belongs to:
[error] ./e8.scala:12:4
[error] The start of this line does not match any of the previous indentation widths.
[error] Indentation width of current line : 3 spaces
[error] This falls between previous widths: 2 spaces and 4 spaces
[error] else
[error] ^
End markers
Indentation has one drawback: it is hard to see where a long region ends, and a closing brace by itself does not say which
region it closes either. An end marker is an optional line made of the identifier end and a following specifier, placed at
the indentation of the construct it closes. It is ordinary syntax that happens to read like a keyword, and end stays an
ordinary identifier everywhere else (the example declares val end = 5).
//> using scala 3.10.0
package trailhead:
enum Segment:
case Path(km: Double)
case Hut(beds: Int)
end Segment
class Route(segments: Vector[Segment]):
def this() =
this(Vector.empty)
end this
def totalKm: Double =
var sum = 0.0
var i = 0
while i < segments.size do
segments(i) match
case Segment.Path(km) => sum += km
case Segment.Hut(_) => ()
end match
i += 1
end while
sum
end totalKm
def beds: Int =
val total =
for
case Segment.Hut(b) <- segments
yield b
end total
total.sum
end beds
end Route
given Ordering[Route] =
Ordering.by(_.totalKm)
end given
extension (r: Route)
def isEmpty: Boolean = r.totalKm == 0.0
end extension
end trailhead
@main def run(): Unit =
import trailhead.*
val r = Route(Vector(Segment.Path(3.5), Segment.Hut(12), Segment.Path(1.5)))
println(r.totalKm)
println(r.beds)
println(r.isEmpty)
println(Route().isEmpty)
val end = 5 // 'end' is an ordinary identifier elsewhere
println(end + 1)
5.0
12
false
true
6
The specifier must correspond to the statement that precedes the marker:
| Preceding statement | Specifier |
|---|---|
defines a member |
the same identifier |
defines a constructor |
|
defines an anonymous given |
|
defines an anonymous extension |
|
defines an anonymous class |
|
a |
|
a package clause for package |
|
an |
that same keyword |
A wrong specifier or a marker at the wrong column is rejected:
[error] ./w7.scala:4:1
[error] misaligned end marker
[error] end Trail2
[error] ^^^^^^^^^^
The same "misaligned end marker" message appears for end while closing an if, or end g closing f.
When to use them
The reference recommends end markers where the extent of a region is not clear at a glance: when the construct contains blank
lines, when it is long (15 to 20 lines or more) or when it ends heavily indented (four levels or more). Otherwise they only
add noise. If several nested regions qualify, mark only the outermost; the cascade in the example above is deliberately
over-marked to show every kind and is not a style to copy. The marker lines themselves are legal in the package, template
and block positions.
Compiler options and rewrites
| Option | Effect |
|---|---|
|
allow significant indentation (the default); with |
|
require classical braces; indentation is not significant but is still checked against the braces (warnings above) |
|
require |
|
require parentheses around conditions; with |
|
rewrite sources in place (combined with one of the options above or with a |
Also, -no-indent, -old-syntax and -source 3.0-migration all turn off significant indentation. The option names above
are the ones used by the reference; each is also available as a compiler setting in sbt (scalacOptions) and as
//> using option in a Scala CLI file.
With significant indentation off, the example from the colon section no longer compiles:
//> using scala 3.10.0
//> using option -no-indent
@main def run(): Unit =
val x = 1
if x > 0 then
println("positive")
[error] ./ni.scala:4:24
[error] expression expected but val found
[error] @main def run(): Unit =
[error] ^
[error] ./ni.scala:6:3
[error] Illegal start of toplevel definition
[error] if x > 0 then
[error] ^^
Rewriting a Scala 2 style file
The two rewrites cannot be combined in one step: -indent only works on new-style control syntax, so a classical file goes
through the compiler twice, first with -rewrite -new-syntax and then with -rewrite -indent. The reverse direction is
-rewrite -no-indent, then -rewrite -old-syntax. The rewrite edits the source in place, so run it on a copy or under version
control. With Scala CLI the options go in //> using option lines. The starting file:
//> using scala 3.10.0
class Trail(val name: String) {
def label(km: Double): String = {
if (km > 10) {
name + " (long)"
} else {
name
}
}
}
object Main {
def main(args: Array[String]): Unit = {
val t = new Trail("Pico")
for (k <- List(4.0, 12.0)) {
println(t.label(k))
}
}
}
After one run with //> using option -rewrite -new-syntax the control syntax changed but the braces remain:
class Trail(val name: String) {
def label(km: Double): String = {
if km > 10 then {
name + " (long)"
} else {
name
}
}
}
object Main {
def main(args: Array[String]): Unit = {
val t = new Trail("Pico")
for k <- List(4.0, 12.0) do {
println(t.label(k))
}
}
}
Changing the option line to -rewrite -indent and running again produced the braceless program, and the program printed
Pico and Pico (long) both before and after:
class Trail(val name: String):
def label(km: Double): String =
if km > 10 then
name + " (long)"
else
name
object Main:
def main(args: Array[String]): Unit =
val t = new Trail("Pico")
for k <- List(4.0, 12.0) do
println(t.label(k))
[patched file .../rw/step1.scala]
Pico
Pico (long)
Going back, -rewrite -no-indent turned the braceless file into braces for classes and methods but left the
if/for bodies in new syntax (they had no indentation block to convert); a second pass with -rewrite -old-syntax
then wrote the conditions as if (km > 10) and for (k <- List(4.0, 12.0)). The Scala 3 migration guide shows the same
two-step flow on a larger example.
Formatting with scalafmt
Scalafmt (3.11.5 at the check date) formats Scala 3 code once runner.dialect is a Scala 3 dialect in .scalafmt.conf, and it
can apply the same rewrites as the compiler:
version = 3.11.5
runner.dialect = scala3
rewrite.scala3.convertToNewSyntax = true
rewrite.scala3.optionalBraces.enabled = true
convertToNewSyntax rewrites if (c), while (c) and for (g) to the quiet form, and also (depending on further flags)
: _* to * and _ imports to *. The rewrite.scala3.optionalBraces section, available since scalafmt 3.8.1, removes
optional braces and applies significant indentation; its sibling rewrite.scala3.endMarker controls inserting and removing
end markers. Older scalafmt releases called the first setting rewrite.scala3.removeOptionalBraces, so check the version
in your build against the configuration page before relying on a name.
Relaxed lambda syntax (SIP-75, preview in 3.10)
Relaxed lambda syntax is a package of small improvements that make function literals after a colon more flexible. It is a
preview feature in Scala 3.10.0: it is switched on with the -preview compiler flag and its details may still change before it
becomes standard. It was experimental in 3.9, so it is not available as a stable feature on the 3.9 LTS line (or the 3.3 LTS).
Related, SIP-74 (parenthesised partial function literals) is also a preview in 3.10. The enabling line in a Scala CLI file is
//> using option -preview; in sbt, add -preview to scalacOptions. These examples were run with Scala 3.10.0.
//> using scala 3.10.0
//> using option -preview
case class Trail(name: String, km: Double)
def fun(f: Int => Int => Int): Int = f(1)(2)
@main def run(): Unit =
val trails = List(Trail("Pico", 12.5), Trail("Lago", 4.0))
val names = trails.map: t => t.name.toUpperCase
println(names)
val pairs = List((1, 2), (3, 4))
val sums = pairs.map: (a, b) => a + b
println(sums)
val firsts = pairs.collect: case (a, b) if b > 2 => a
println(firsts)
val inParens = pairs.collect(case (a, b) if b > 2 => a)
println(inParens)
val curried = fun: (x: Int) => y =>
x + y
println(curried)
val partial: PartialFunction[(Int, Int), Int] = case (a, b) if b > 2 => a
println(partial.isDefinedAt((1, 1)))
val nested = trails.map: t => t.name + trails.count(u => u.km > t.km)
println(nested)
List(PICO, LAGO)
List(3, 7)
List(3)
List(3)
3
false
List(Pico0, Lago1)
The three parts are:
-
Single-line lambdas. A lambda that follows a colon on the same line,
xs.map: x => x + 1, was not accepted before (the arrow had to be followed by a newline and an indented block). The lambda extends to the end of the line. It works with one or more parameters, with type parameters, and with a singlecase. It does not work for function values without an arrow:xs.map: _ + _andxs.map: plus1are errors. The new forms do not apply to code that is immediately enclosed in parentheses, because(x: Int => Int)there is a type ascription. -
Curried multi-line lambdas. Several parameter sections, each followed by an arrow, can open one indented block:
fun: (x: Int) => y =>as in the example. -
Case expressions. A single
caseclause can be written inside parentheses or as an expression, as in thecollect(case ...)call and thePartialFunctionvalue above. SIP-74 specifies this part.
Without -preview the same code is rejected on 3.10.0, with messages that do not mention the missing flag:
//> using scala 3.10.0
@main def run(): Unit =
val xs = List(1, 2)
val ys = xs.map: x => x + 1
println(ys)
[error] ./e4.scala:4:12
[error] not a legal formal parameter for a function literal
[error] val ys = xs.map: x => x + 1
[error] ^^^^^^^^^
[error] ./e4.scala:4:25
[error] Not found: x
[error] val ys = xs.map: x => x + 1
[error] ^
The reference text says single-line lambdas can nest (xs.map: x => x.toString + xs.dropWhile: y => y > 0), but Scala
3.10.0 rejects exactly that line and asks for parentheses around the second lambda:
[error] ./n.scala:5:50
[error] Can't have two single line lambdas on the same line, enclose the second in parentheses
[error] val ys = xs.map: x => x.toString + xs.dropWhile: y => y > 0
[error] ^
The example above uses trails.count(u => ...) for
that reason. Preview features are described on
Language Evolution: SIPs, Preview & Experimental,
and function values in general on Functions as Values.
Source: SIP-75: Relaxed lambda syntax
Comparing with other languages
| Language | Block structure | Scala 3 counterpart |
|---|---|---|
Scala 2 |
braces only; |
the same code compiles unchanged; braces and parentheses stay legal |
Java |
braces; |
|
Kotlin |
braces; |
trailing lambda with |
Python |
indentation; a colon ends every compound statement header |
a colon only where a template body or an argument starts; |
The trailing-lambda feel is the closest parallel to Kotlin (xs.map { x -> ... }) and Groovy closures. For comparison see
Java lambdas and method references and
Java control flow. Python’s mandatory indentation is described on
Python lexical structure and style. Unlike Python, Scala’s
indentation is optional (braces always work) and a mismatch is a compile-time warning or error, not a different program.
Pitfalls
-
Mixing tabs and spaces. The error shown above; configure the editor to insert spaces.
-
Forgetting
thenordo.if x > 0 println(...)gives "'then' expected". -
A line that looks aligned but is not. A line indented between two earlier widths is rejected ("does not match any of the previous indentation widths").
-
Aligned versus indented continuation.
f(x)followed by(y)at the same column is two statements; indented, it is a single call. -
Colons. A trailing colon after a call opens an argument and a trailing colon after a class, trait, object or enum header opens its body; a colon after any other statement is a syntax error.
-
Misaligned end marker. The marker must be on its own line at the column of the construct it closes and name it exactly.
-
-no-indentleftovers. Code written braceless fails to compile under-no-indent; old code stays valid under indentation. -
Preview flag. Relaxed lambdas need
-previewon 3.10; the errors without it do not mention the flag.
Where the book is outdated
Books written for Scala 2 show only braces and parentheses; they do not cover then/do, a colon for template bodies, end
markers or fewer braces, all of which are valid Scala 3 and sometimes the form used by newer documentation. Early Scala 3 material
(2021 and 2022) describes "fewer braces" as experimental, requiring import language.experimental.fewerBraces; the import is
unnecessary since the feature became stable in 3.3. The given ... with form for instances and the : _* vararg splice
are older spellings that newer code replaces with a trailing colon and *.