Scala 3: Even Fewer Braces

Dean Wampler
Scala 3
Published in
5 min readNov 20, 2022

--

One of the most significant, if controversial features in Scala 3 is optional braces, where significant indentation can be used instead of curly braces. However, there were a few syntax areas where braces were still required, such as passing anonymous functions and import statements. Scala 3.3 will remove the requirement that braces have to be used to pass anonymous functions.

Fall Colors in Lincoln Park, Chicago, © 2022, Dean Wampler

In essence, a colon, :, token is now also recognized as a marker where an anonymous function argument is defined next.

Let’s look at some examples, which I have added to the Programming Scala code repo. They are adapted from the Dotty documentation page:
https://dotty.epfl.ch/docs/reference/other-new-features/indentation.html

The changes we will explore are already available as an experimental feature in Scala 3.2, which means that a special, experimental import statement is required.

However, the Scala compiler disallows use of experimental features in code when compiling with release builds. Instead, we need to use a snapshot release of the compiler. So, let’s see how to do that first. The steps are straightforward for our purposes:

  1. Clone the Dotty repo: https://github.com/lampepfl/dotty
  2. Run the command: sbt dist/packArchive
  3. Copy the dist/target/*.zip or dist/target/*.tar.gz file that is created somewhere convenient. For me, * was scala3–3.3.0-RC1-bin-SNAPSHOT.
  4. Change to the new directory and expand the archive.
  5. Change to the root directory created for the expanded archive. For me, this was scala3–3.3.0-RC1-bin-SNAPSHOT.
  6. Run the command bin/scala

Now we can copy and past the following examples.

First, we need to enable the feature:

import language.experimental.fewerBraces

This will not be needed starting with the 3.3 release. Actually, you might try omitting this import first, paste the “braceless” examples below, and see what errors are reported. I’ll just proceed with the “happy path”.

I’ll repeat each example twice, once with braces, as required for Scala 3.0 to 3.2, and again with the new syntax that eliminates the braces. This makes it easier to compare the differences.

First, here is an examplemethod that takes a function argument, which takes no arguments:

def times(n: Int)(f: => Unit): Unit =
for i <- 0 until n do f

// Use braces to define and pass the function:
times(3) {
println("one")
println("two")
}
// (Output shown as comments)
// one
// two
// one
// two
// one
// two

// New braceless syntax to define and pass the function. Now the compiler
// interprets the trailing colon, followed by indented lines, as the
// beginning and definition of the anonymous function:
times(3):
println("one")
println("two")
// one
// two
// ...

The output of each block of code is shown as comments. The first invocation uses braces. We see the required pattern in the second invocation; a colon followed by indented lines for the anonymous function body.

It works the same way for methods on types. Consider the ++ method on sequences:

import java.io.File
val dir = new File(".")
// val dir: java.io.File = .

// Another example, where the `++` method expects a function argument:
val paths1 = Seq(dir) `++` {
dir.listFiles
}
// val paths1: Seq[java.io.File] = List(., ./scala3-3.3.0-RC1-bin-SNAPSHOT, ./scala3-3.3.0-RC1-bin-SNAPSHOT.zip)

val paths2 = Seq(dir) `++`:
dir.listFiles
// val paths2: Seq[java.io.File] = List(., ./scala3-3.3.0-RC1-bin-SNAPSHOT, ./scala3-3.3.0-RC1-bin-SNAPSHOT.zip)

What about functions that take arguments? Let’s look at the familiar map and foldLeft methods:

val xs = 0 until 10
// val xs: Range = Range 0 until 10

// The function arguments can either go on the next line after the colon
// or on the same line:
val map1a = xs.map {
x =>
val y = x - 1
y * y
}
// val map1a: IndexedSeq[Int] = Vector(1, 0, 1, 4, 9, 16, 25, 36, 49, 64)

val map2a = xs.map:
x =>
val y = x - 1
y * y
// val map2a: IndexedSeq[Int] = Vector(1, 0, 1, 4, 9, 16, 25, 36, 49, 64)

It’s not especially nice having the argument list on a line by itself. Fortunately, this isn’t required. We can put the x => after the : on the same line:

val map1b = xs.map { x =>
val y = x - 1
y * y
}
// val map1b: IndexedSeq[Int] = Vector(1, 0, 1, 4, 9, 16, 25, 36, 49, 64)

val map2b = xs.map: x =>
val y = x - 1
y * y
// val map2b: IndexedSeq[Int] = Vector(1, 0, 1, 4, 9, 16, 25, 36, 49, 64)

// It looks odd, but the arrow can be on the next line:
val map3b = xs.map: x
=>
val y = x - 1
y * y
// val map3b: IndexedSeq[Int] = Vector(1, 0, 1, 4, 9, 16, 25, 36, 49, 64)

The last example shows we can still put the => on a new line if we want, but it looks weird.

Functions that take more than one argument work the same:

val fold1a = xs.foldLeft(0) { (x, y) =>
x + y
}
// val fold1a: Int = 45

val fold2a = xs.foldLeft(0): (x, y) =>
x + y
// val fold2a: Int = 45

Okay, so we can put the argument list on the same line as the :, but what about the single expression for the body?

// scala> val fold2b = xs.foldLeft(0): (x, y) => x + y
// |
// -- Error: ---------------------------------------------------------------------------------------------------------------------------------------------------
// 1 |val fold2b = xs.foldLeft(0): (x, y) => x + y
// | ^^^^^^^^^^^^^^^^^^^^^^
// | not a legal formal parameter for a function literal

That isn’t supported. If you want everything on one line like this, you’ll have to use braces:

val fold1b = xs.foldLeft(0) {(x, y) => x + y}
// val fold1b: Int = 45

So, if you really dislike colons, you’ll be able to avoid them when passing anonymous functions to methods.

What about import statements?

scala> import scala.util:    # : instead of .??
| Try, Success, Failure
|
-- Error: ---------------------------------------------------------------------------------------------------------------------------------------------------
1 |import scala.util:
| ^
| end of statement expected but ':' found

scala> import scala.util.: # : after .??
| Try, Success, Failure
|
-- [E040] Syntax Error: -------------------------------------------------------------------------------------------------------------------------------------
1 |import scala.util.:
| ^
| an identifier expected, but ':' found
|
| longer explanation available when compiling with `-explain`
-- [E040] Syntax Error: -------------------------------------------------------------------------------------------------------------------------------------
2 | Try, Success, Failure
| ^
| '.' expected, but ',' found
-- [E040] Syntax Error: -------------------------------------------------------------------------------------------------------------------------------------
3 |
|^
|'.' expected, but eof found

Nope. You’ll still need braces:

scala> import scala.util.{Try, Success, Failure}
|

scala> import scala.{
| Option, Some, None
| }
|

Final Thoughts

It’s nice to have the anonymous function “loophole” in the indentation syntax fixed. I’ll let you decide if this syntax is easier to read and understand versus the traditional syntax with braces.

For a concise summary of the more “mainstream” notable changes in Scala 3, see my Scala 3 Highlights page.

See Programming Scala, Third Edition for a comprehensive introduction to Scala 3, including details on how to migrate from Scala 2.

--

--

Dean Wampler
Scala 3

The person who is wrong on the Internet. ML/AI and FP enthusiast. Lurks at the AI Alliance and IBM Research. Speaker, author, pretend photographer.