1. Introduction

Scala 3 added a so-called “quiet syntax”, which allows dropping the number of curly braces and parentheses in conditions and loops. In this tutorial, we’ll see several examples of quiet syntax usage in Scala 3, along with their Scala 2 equivalents for comparison.

2. Significant Indentation

Significant indentation, ala Python, allows reducing the number of braces used for flow control. Let’s see an example method definition using Scala 3’s quiet syntax, followed by the corresponding definition in Scala 2:

def luckyNumberScala3(): Int =
  println("Returning lucky number...")

  7

def luckyNumberScala2(): Int = {
  println("Returning lucky number...")

  7
}

As we can see, indentation is substituted for the usage of curly braces, which are entirely dropped in the quiet syntax. Let’s check out the different tokens that we can break a line after and take advantage of quiet syntax indentation:

= => ? => <- catch do else finally for if match return then throw try while yield

Let’s write a match expression with the new syntax:

7 match // Scala 3
  case 7 => "Lucky number"
  case _ => "Non lucky number"

7 match { // Scala 2
  case 7 => "Lucky number"
  case _ => "Non lucky number"
}

We can also use quiet syntax when creating classes, traits, and objects:

case class AClassScala3():
  def doSomething(): Unit = ???

object AClassScala3:
  def apply(): AClassScala3 = ???

trait ATraitScala3:
  def doSomethingElsee(): Unit

case class AClassScala2() {
  def doSomething(): Unit = ???
}

object AClassScala2 {
  def apply(): AClassScala2 = ???
}

trait ATraitScala2 {
  def doSomethingElsee(): Unit
}

Partial functions also benefit from the new syntax:

val positiveIntScala3: Int => Int =
  case x if x > 0 => x

val positiveIntScala2: Int => Int = {
  case x if x > 0 => x
}

Scala 3 supports both tabs and spaces for indentation, however, it is best practice to pick one and be consistent in the code.

3. Simplified Loops and Conditional Statements

Loops and conditional statements can take advantage of the revised syntax as well. Let’s explore a couple of examples to check the differences:

for i <- 0 until 1 do println(i) // Scala 3
for (i <- 0 until 1) println(i)  // Scala 2

if 5 % 2 == 0 then println("even") // Scala 3
else println("odd")

if (5 % 2 == 0) println("even") // Scala 2
else println("odd")

The new keywords do and then replace parentheses for inline conditional statements. The new syntax doesn’t always lead to less typing, as in the case of then. However, it might be more readable and expressive. As always, though, consistency in the codebase is most important.

Finally, let’s look at a for comprehension:

for i <- 0 until 5 // Scala 3
  if i % 2 == 1
    iWasOdd = 2 * i
yield iWasOdd

for {
  i <- (0 until 5) // Scala 2
  if (i % 2 == 1)
    iWasOdd = 2 * i
} yield iWasOdd

In the above case, we wrote the same code, arguably with more clarity, with 11.25% fewer characters. Not bad at all!

4. New Import Style

Imports have been modified as well. Let’s compare some examples:

import scala.collection.*                   // Scala 3
import scala.collection._                   // Scala 2

import scala.collection.mutable.Map as MMap   // Scala 3
import scala.collection.mutable.{Map => MMap} // Scala 2

import java.util.{Random as _, *}             // Scala 3
import java.util.{Random => _, _}             // Scala 2

The main differences to observe from the above examples are, in order:

  • The * replaces _
  • The keyword “as“ replaces => and curly braces are not needed anymore
  • The _ is still used to hide an import, however, the last example shows how to use it in conjunction with * to hide a single import

5. Moving to the New Syntax

Manually moving to the new syntax would be really impractical. The Scala compiler has some new features to allow total or partial migration to the new syntax.

Let’s migrate a simple case class:

$ cat IWasScala2.scala
case class IWasScala2() {
  def doStuff: Unit = ???
}

$ scalac  -indent  -rewrite IWasScala2.scala
[patched file IWasScala2.scala]

$ cat IWasScala2.scala
case class IWasScala2():
  def doStuff: Unit = ???

There are four new parameters in the compiler that can be used for the migration or to enforce a particular syntax (check the compiler documentation for more details):

  • -new-syntax: enforce new control syntax (then and do) in control expressions
  • -old-syntax: enforce usage of parentheses around conditions
  • -indent: allow usage of significant indentation
  • -no-indent: enforce non-significant indentation

We can try to enforce the new syntax on an old class to see what happens:

$ cat DoError.scala
case class DoError() {
  def doStuff: Unit = { if (2 > 0) { println("ok") } }
}

$ scalac -new-syntax DoError.scala
-- Error: DoError.scala:2:29 ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
2 |  def doStuff: Unit = { if (2 > 0) { println("ok") } }
  |                           ^^^^^^^
  |                           This construct is not allowed under -new-syntax.
  |                           This construct can be rewritten automatically under -new-syntax -rewrite -source 3.0-migration.
1 error found

$ scalac -new-syntax -rewrite DoError.scala
[patched file DoError.scala]

$ cat DoError.scala
case class DoError() {
  def doStuff: Unit = { if 2 > 0 then { println("ok") } }
}

The syntax error is clearly marked by the compiler, and a suggestion on how to automatically migrate to the new syntax is also reported in the message.

6. Conclusion

As we’ve seen in different examples, in some cases the new syntax can be more expressive and can save some typing. This is not always the case, however. Sometimes it’s just a question of habit. However, as we repeatedly remarked, the importance is to maintain consistency throughout the codebase.

As always, all the code seen in this article is available over on GitHub.


« 上一篇: ZIO简介
» 下一篇: 在Scala中创建列表