Scala 3 Migration Guide

Dropped Features

Language

Some features are dropped to simplify the language. Most of these changes can be handled automatically during the Scala 3 migration compilation.

Incompatibility Scala 2.13 Scala 3 Migration Rewrite Scalafix Rule
Symbol literals Deprecation  
do-while construct    
Auto-application Deprecation
Value eta-expansion Deprecation
any2stringadd conversion Deprecation  
Early initializer Deprecation    
Existential type Feature warning    
@specialized Deprecation    

Symbol literals

The Symbol literal syntax is deprecated in Scala 2.13 and dropped in Scala 3. But the scala.Symbol class still exists so that each string literal can be safely replaced by an application of Symbol.

This piece of code cannot be compiled with Scala 3:

val values: Map[Symbol, Int] = Map('abc -> 1)

val abc = values('abc) // In Scala 3, Migration Warning: symbol literal 'abc is no longer supported

The Scala 3 migration compilation rewrites the code into:

val values: Map[Symbol, Int] = Map(Symbol("abc") -> 1)

-val abc = values('abc)
+val abc = values(Symbol("abc"))

Although the Symbol class is useful during the transition, beware that it is deprecated and will be removed from the scala-library in a future version. You are recommended, as a second step, to replace every use of Symbol with a plain string literals "abc" or a custom dedicated class.

do-while construct

The do keyword has acquired a different meaning in the New Control Syntax.

To avoid confusion, the traditional do <body> while (<cond>) construct is dropped. It is recommended to use the equivalent while ({ <body>; <cond> }) () that can be cross-compiled, or the new Scala 3 syntax while { <body>; <cond> } do ().

The following piece of code cannot be compiled with Scala 3.

do { // In Scala 3, Migration Warning: `do <body> while <cond>` is no longer supported
  i += 1
} while (f(i) == 0)

The Scala 3 migration compilation rewrites it into.

while ({ {
  i += 1
} ; f(i) == 0}) ()

Auto-application

Auto-application is the syntax of calling an empty-paren method such as def toInt(): Int without passing an empty argument list. It is deprecated in Scala 2.13 and dropped in Scala 3.

The following code is invalid in Scala 3:

object Hello {
  def message(): String = "Hello"
}

println(Hello.message) // In Scala 3, Migration Warning: method message must be called with () argument

The Scala 3 migration compilation rewrites it into:

object Hello {
  def message(): String = "Hello"
}

-println(Hello.message)
+println(Hello.message())

Auto-application is covered in detail in this page of the Scala 3 reference documentation.

Value eta-expansion

Scala 3 introduces Automatic Eta-Expansion which will deprecate the method to value syntax m _. Furthermore Scala 3 does not allow eta-expansion of values to nullary functions anymore.

Thus, this piece of code is invalid in Scala 3:

val x = 1
val f: () => Int = x _ // In Scala 3, Migration Warning: The syntax `<function> _` is no longer supported;

The Scala 3 migration compilation rewrites it into:

val x = 1
-val f: () => Int = x _
+val f: () => Int = (() => x)

any2stringadd conversion

The implicit Predef.any2stringadd conversion is deprecated in Scala 2.13 and dropped in Scala 3.

This piece of code does not compile anymore in Scala 3.

val str = new AnyRef + "foo" // In Scala 3, Error: value + is not a member of Object

The conversion to String must be applied explicitly, for instance with String.valueOf.

-val str = new AnyRef + "foo"
+val str = String.valueOf(new AnyRef) + "foo"

This rewrite can be applied by the fix.scala213.Any2StringAdd Scalafix rule in scala/scala-rewrites.

Early Initializer

Early initializers are deprecated in Scala 2.13 and dropped in Scala 3. They were rarely used, and mostly to compensate for the lack of Trait parameters which are now supported in Scala 3.

That is why the following piece of code does not compile anymore in Scala 3.

trait Bar {
  val name: String
  val size: Int = name.size
}

object Foo extends {
  val name = "Foo"
} with Bar

The Scala 3 compiler produces two error messages:

-- Error: src/main/scala/early-initializer.scala:6:19 
6 |object Foo extends {
  |                   ^
  |                   `extends` must be followed by at least one parent
-- [E009] Syntax Error: src/main/scala/early-initializer.scala:8:2 
8 |} with Bar
  |  ^^^^
  |  Early definitions are not supported; use trait parameters instead

It suggests to use trait parameters which would give us:

trait Bar(name: String) {
  val size: Int = name.size
}

object Foo extends Bar("Foo")

Since trait parameters are not available in Scala 2.13, it does not cross-compile. If you need a cross-compiling solution you can use an intermediate class that carries the early initialized vals and vars as constructor parameters.

abstract class BarEarlyInit(val name: String) extends Bar

object Foo extends BarEarlyInit("Foo")

In the case of a class, it is also possible to use a secondary constructor with a fixed value, as shown by:

class Fizz private (val name: String) extends Bar {
  def this() = this("Fizz")
}

Another use case for early initializers in Scala 2 is private state in the subclass that is accessed (through an overridden method) by the constructor of the superclass:

class Adder {
  var sum = 0
  def add(x: Int): Unit = sum += x
  add(1)
}
class LogAdder extends {
  private var added: Set[Int] = Set.empty
} with Adder {
  override def add(x: Int): Unit = { added += x; super.add(x) }
}

This case can be refactored by moving the private state into a nested object, which is initialized on demand:

class Adder {
  var sum = 0
  def add(x: Int): Unit = sum += x
  add(1)
}
class LogAdder extends Adder {
  private object state {
    var added: Set[Int] = Set.empty
  }
  import state._
  override def add(x: Int): Unit = { added += x; super.add(x) }
}

Existential Type

Existential type is a dropped feature, which makes the following code invalid.

def foo: List[Class[T]] forSome { type T } // In Scala 3, Error: Existential types are no longer supported

Existential type is an experimental feature in Scala 2.13 that must be enabled explicitly either by importing import scala.language.existentials or by setting the -language:existentials compiler flag.

In Scala 3, the proposed solution is to introduce an enclosing type that carries the dependent type:

trait Bar {
  type T
  val value: List[Class[T]]
}

def foo: Bar

Note that using a wildcard argument, _ or ?, is often simpler but is not always possible. For instance you could replace List[T] forSome { type T } by List[?].

Specialized

The @specialized annotation from Scala 2 is ignored in Scala 3.

However, there is limited support for specialized Function and Tuple.

Similar benefits can be derived from inline declarations.

Contributors to this page: