Incompatibilities & Dropped Features
|
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, released 2026-10-12). Every program, diagnostic and rewrite on it was produced with Scala CLI 1.17.1 on JDK 26 on 2026-10-16. Where a result depends on another compiler it was produced with Scala 3.9.0 (the LTS line), 3.3.8 (the previous LTS) or 2.13.18, each fetched from Maven Central through the same Scala CLI. The page answers one question: what stops a piece of Scala 2.13 code, or a pattern remembered from an older book, from compiling or from behaving the same under Scala 3.10, and what replaces it.
The official migration guide groups the answers into an incompatibility table (syntactic changes, dropped features, contextual abstractions, other changed features, type checker, type inference and macros), and the language reference has a separate dropped features chapter. Both are covered row by row below, then merged with the changes that Scala 3 itself made between 3.3 and 3.10, and finally with the outdated forms found in books. Planning a whole migration is the subject of Migrating from Scala 2; which compiler can read whose output is covered in Scala 2 & Scala 3 Compatibility; moving between Scala 3 versions is in Upgrading Between Scala 3 Versions. This page is the reference those pages point to.
The examples use the trailhead scenario (trails, huts and a Planner) where it fits, and a minimal standalone program
where it does not. Each failing example shows the exact compiler message from 3.10.0, with the file path removed.
What counts as an incompatibility
An incompatibility is a piece of code that compiles with Scala 2.13 and does not compile with Scala 3. Two refinements matter in practice:
-
A runtime incompatibility is code that compiles on both but behaves differently. They are rare and dangerous, so this page flags every one it knows: implicit views (Implicit views), the inferred type of an overriding method (Return type of an override), a
->defined through an implicit class (->is now an extension method), andprivate[this]fields (private[this]andprotected[this]). -
A removal is not always an incompatibility in the strict sense. Scala 3.0 to 3.3 kept some Scala 2 syntax alive for migration, then Scala 3.4 started to warn about it and Scala 3.10 turned some of those warnings into errors, so code that was fine on the 3.3 LTS can stop compiling on 3.10 even though it is Scala 3 code. The timeline is in From warnings to errors: the 3.3 to 3.10 timeline.
Three outcomes appear in the tables below. Error means the compiler rejects the code on 3.10. Warning means it compiles and the message names the replacement. Silent means it compiles with no message at the default settings.
At a glance
| Incompatibility | Scala 3.10 result | Auto-rewrite | Replacement |
|---|---|---|---|
Syntactic changes |
|||
Restricted keywords ( |
error |
migration mode |
backticks or another name |
error |
migration mode, Scalafix |
|
|
error |
migration mode |
|
|
error |
migration mode |
brace on the line of the call |
|
error |
none |
fix the indentation |
|
error |
none |
a real name such as |
|
error |
none |
a real name such as |
|
Dropped features |
|||
error |
migration mode |
|
|
error |
migration mode |
|
|
error |
migration mode, Scalafix |
write |
|
error |
migration mode, Scalafix |
|
|
error |
Scalafix |
|
|
error |
none |
trait parameters |
|
error |
none |
|
|
silent, ignored |
none |
|
|
Contextual abstractions |
|||
error |
Scalafix |
write the type |
|
error, may change behavior |
none |
call the conversion |
|
error |
none |
implicit parameter or |
|
error |
none |
call the conversion |
|
Other changed features |
|||
error |
migration mode |
|
|
error |
warning only |
make it |
|
error |
none |
remove the abstract |
|
error |
none |
|
|
error |
none |
pattern binding |
|
error |
none |
|
|
error |
none |
own method or inlining |
|
error |
none |
type parameter or wrapper |
|
Type checker |
|||
error |
none |
invariance, bounds, overload |
|
error |
none |
relax the types |
|
Type inference |
|||
inferred type changes |
none |
write the type |
|
error on use |
none |
write the structural type |
|
Macros |
|||
error |
none |
|
|
Letting the compiler do the work first
Most syntactic and dropped-feature rows have an automatic rewrite. It lives in the compiler’s migration mode:
-source:3.0-migration turns the hard errors of the rows above into migration warnings and, together with -rewrite,
patches the files in place. Run it on a clean working tree so that git diff shows exactly what changed.
The source file below packs eight rows of the table into Scala 2 style code. It is deliberately brace-based, the style the rewrite expects.
//> using scala 3.10.0
object given {
val enum = 1
}
class Trail {
val km = 2
}
object Walk {
val km = 9
class Day extends Trail {
def show(): Unit = println(km)
}
def print() { println("walk") }
val labels = Map('easy -> 1)
val square = { x: Int => x * x }
val ascent = 40
val lazyAscent: () => Int = ascent _
def message(): String = "hello"
def run(): Unit = {
var i = 0
do {
i += 1
} while (i < 3)
println(message)
println(i)
}
}
Compiling it with the default settings fails with eight errors, one per row (the messages are shown in the sections
below). Compiling it with scala compile --scalac-option -rewrite --scalac-option -source:3.0-migration Legacy.scala
prints a single migration warning for the auto-application and the line [patched file Legacy.scala]. This is the diff
the rewrite produced:
-object given {
- val enum = 1
+object `given` {
+ val `enum` = 1
...
- def show(): Unit = println(km)
+ def show(): Unit = println(this.km)
...
- def print() { println("walk") }
+ def print(): Unit = { println("walk") }
- val labels = Map('easy -> 1)
- val square = { x: Int => x * x }
+ val labels = Map(Symbol("easy") -> 1)
+ val square = { (x: Int) => x * x }
...
- val lazyAscent: () => Int = ascent _
+ val lazyAscent: () => Int = (() => ascent)
...
- do {
+ while ({ {
...
- } while (i < 3)
- println(message)
+ } ; i < 3}) ()
+ println(message())
The patched file compiles on 3.10 with exit code 0. The rewritten code is correct but not pretty (while ({ { ... } ; c }) ()
is the literal desugaring of do), so review it as ordinary source.
Two remarks about the rewrite. First, it only repairs rows marked "migration mode" in the table above; the other rows need
a human. Second, a rewrite run does not replay earlier ones: -source:3.4-migration patches only what became deprecated
in 3.4 (see From warnings to errors: the 3.3 to 3.10 timeline), not the 3.0 items. The Scala 2.13 compiler can find the same constructs before migration starts
when run with -Xsource:3, and Scalafix has rules for the rows marked "Scalafix" (procedure syntax, auto-application,
eta-expansion, any2stringadd, explicit implicit types). Scalafix is a third-party tool from the Scala Center; it is not
part of the Scala distribution. See
Migrating from Scala 2 for the full workflow.
The same code on 2.13.18
To make the "compiles on 2.13" claim concrete, the next program mixes procedure syntax, a symbol literal, a view bound, an
early initializer, an implicit definition without a type, auto-application, any2stringadd, a bean property, the
curried companion of a case class and a do loop. It was run with //> using scala 2.13.18 and
//> using options -deprecation -feature.
warning: procedure syntax is deprecated: instead, add `: Unit =` to explicitly declare `proc`'s return type [quickfixable]
warning: symbol literal is deprecated; use Symbol("abc") instead [quickfixable]
warning: view bounds are deprecated; use an implicit parameter instead.
warning: early initializers are deprecated; they will be replaced by trait parameters in 3.0
warning: Implicit definition should have explicit type (inferred Int) [quickfixable]
warning: Auto-application to `()` is deprecated. Supply the empty argument list `()` explicitly to invoke method message
warning: method any2stringadd in object Predef is deprecated (since 2.13.0): Implicit injection of + is deprecated.
7 warnings
procedure syntax
hello
3
16
3
Symbol(abc)
Foo(1,true)
x
true
1
3
Seven warnings and a successful run. Every warning points to the replacement that Scala 3 requires, so a 2.13 build with
-deprecation (and -Xsource:3) is the cheapest preparation for the move. Not every row has a 2.13 warning: the do
loop, the lambda without parentheses, the bean property and the case-class companion functions compiled silently in this
program and fail on 3.
Syntactic changes
Scala 3 introduced optional braces and the new control syntax. That costs a few restrictions on the older syntax and removes some oddities. The migration guide lists seven rows.
Restricted keywords
enum, export, given, then, =>> and ?=> are regular keywords in Scala 3 and cannot be identifiers any more
(soft keywords such as inline, opaque, extension, using, open, transparent and infix are unaffected). The
migration guide’s example is object given { val enum = ... }:
//> using scala 3.10.0
object given:
val enum = 1
println(enum)
-- [E040] Syntax Error: kw.scala:3:7
3 |object given:
| ^^^^^
| an identifier expected, but 'given' found
-- [E103] Syntax Error: kw.scala:5:2
5 | println(enum)
| ^^^^^^^
| Illegal start of toplevel definition
2 errors found
The fix is a different name or backticks around the name, and the migration rewrite chooses backticks (see the diff
above, where both given and enum are quoted). A Java method with such a name, for example then, is called with the
same backticks, but the practical remedy is to rename on your side.
Procedure syntax
def print() { ... }, with no =, declared a method returning Unit in Scala 2. It was deprecated in 2.13 and is
dropped in Scala 3.
//> using scala 3.10.0
object Bar:
def print() {
println("bar")
}
-- [E040] Syntax Error: proc.scala:4:14
4 | def print() {
| ^
| '=' expected, but '{' found
1 error found
Write def print(): Unit = .... The rewrite inserts : Unit = (see the diff above). Note that the form made a method
return Unit whatever its body computed.
Parentheses around a lambda parameter
A lambda parameter followed by a type must be parenthesized: { x: Int => x * x } is ambiguous with the optional-braces
syntax and is rejected.
-- Error: lambda.scala:3:17
3 |val f = { x: Int => x * x }
| ^
|parentheses are required around the parameter of a lambda
|This construct can be rewritten automatically under -rewrite -source 3.0-migration.
1 error found
The fix is { (x: Int) => x * x }, which also compiles on 2.13, so the change can be made before the migration. The
message itself tells you the rewrite switch.
Open brace indentation for passing an argument
A Scala 2 style that puts the argument block on the next line, after the call, is no longer parsed as an argument.
//> using scala 3.10.0
def test(name: String)(body: => Unit): Unit = body
@main def braceDemo(): Unit =
test("my test")
{
assert(1 == 1)
}
On 3.10 the brace is a new statement, and the call before it lacks its second argument list:
-- [E178] Type Error: brace.scala:6:2
6 | test("my test")
| ^^^^^^^^^^^^^^^
| missing argument list for value of type (=> Unit) => Unit
1 error found
The migration rewrite indents the brace so that it belongs to the call; the cleaner fix is to move it up:
test("my test") {. The same rewrite handles a refinement placed after a new line (type Bar = Foo followed by {).
Wrong indentation
Scala 3 uses indentation to find the end of a statement more strictly. In the guide’s example an indented line after a value is read as an application of that value, not a new statement:
//> using scala 3.10.0
def bar: (Int, Int) = {
val foo = 1.0
val bar = foo
(1, 1)
}
-- [E050] Type Error: indent.scala:5:12
5 | val bar = foo
| ^^^
| value foo does not take parameters
6 | (1, 1)
-- [E007] Type Mismatch Error: indent.scala:7:1
7 |}
| ^
|Found: Unit
|Required: (Int, Int)
|
|Maybe the enclosing block is missing a final expression after the definition of `bar`?
2 errors found
There is no automatic rewrite; fix the indentation so that (1, 1) lines up with val. A formatter such as scalafmt
prevents the mistake, but it may restyle the whole project, so adopt it as a separate commit. The rest of the indentation
rules are in Optional Braces & Indentation.
_ as a type parameter
Scala 2 accepted def foo[_: Foo], a type parameter named _ together with a context bound, a trick the fastparse
library used. Martin Odersky called it a "clever exploit of a scalac compiler bug", and Scala 3 closes it.
-- [E040] Syntax Error: under.scala:4:8
4 |def foo[_: Foo]: Unit = ()
| ^
| an identifier expected, but '_' found
1 error found
Give the parameter a real name (def foo[T: Foo]). Binary compatibility is unaffected because type parameters are
erased.
+ and - as type parameters
def foo[+] named a type parameter + in Scala 2. Scala 3 reads the sign as a variance annotation and finds no name.
-- Error: plusminus.scala:3:8
3 |def foo[+]: Unit = ()
| ^
| no `+/-` variance annotation allowed here
1 error found
Choose an ordinary name. + and - remain valid as type names in general (type +), only the parameter position changed.
Dropped features
The second group of rows removes features "to simplify the language". Eight rows, all of which fail on 3.10.
Symbol literals
'abc was a literal of type Symbol in Scala 2. The syntax is deprecated in 2.13 and gone in Scala 3, where ' begins
a quote for metaprogramming. The class scala.Symbol itself still exists.
//> using scala 3.10.0
val values: Map[Symbol, Int] = Map('abc -> 1)
val abc = values('abc)
-- Error: sym.scala:3:35
3 |val values: Map[Symbol, Int] = Map('abc -> 1)
| ^
|symbol literal 'abc is no longer supported,
|use a string literal "abc" or an application Symbol("abc") instead,
|or enclose in braces '{abc} if you want a quoted expression.
|For now, you can also `import language.deprecated.symbolLiterals` to accept
|the idiom, but this possibility might no longer be available in the future.
-- Error: sym.scala:4:17
4 |val abc = values('abc)
| ^
|symbol literal 'abc is no longer supported,
...
2 errors found
The rewrite produces Symbol("abc"). The migration guide recommends a second step: replace Symbol with plain strings or
a dedicated class, because Symbol itself is deprecated. The message names the escape hatch
import language.deprecated.symbolLiterals, which keeps old files compiling while they are being converted.
do-while
do now introduces the body of for, while and if in the new control syntax, so do { ... } while (cond) could not
stay. The program below shows what 3.10 says:
//> using scala 3.10.0
@main def doWhileDemo(): Unit =
var i = 0
do {
i += 1
} while (i < 3)
println(i)
-- Error: dowhile.scala:5:2
5 | do {
| ^^
| end of toplevel definition expected but 'do' found
-- [E103] Syntax Error: dowhile.scala:8:2
8 | println(i)
| ^^^^^^^
| Illegal start of toplevel definition
2 errors found
The replacements are while ({ body; cond }) (), which also compiles on Scala 2 and is what the rewrite emits, or the
Scala 3 form below. The loop-and-a-half style, a while whose condition block runs the body first, is the idiom:
//> using scala 3.10.0
@main def whileDemo(): Unit =
var i = 0
while
i += 1
i < 3
do ()
println(i)
which prints 3. For a loop that walks a collection, prefer for or a higher-order function; see
Conditionals & Loops.
Auto-application
A method declared with an empty parameter list, def message(): String, could be called as message in Scala 2.
Scala 3 requires the parentheses; Java-defined methods are exempt, because their signatures cannot express the
difference.
//> using scala 3.10.0
object Hello:
def message(): String = "Hello"
@main def autoDemo(): Unit =
println(Hello.message)
println("abc".length)
-- [E100] Syntax Error: autoapp.scala:7:16
7 | println(Hello.message)
| ^^^^^^^^^^^^^
| method message in object Hello must be called with () argument
1 error found
Only the first call fails: "abc".length calls a Java method and compiles. The rewrite adds the ()
(println(message()) in the diff above) and Scalafix can do it on 2.13 beforehand. The convention that follows is the
one the style guide gives: use () for methods with side effects and no () for pure accessors.
Value eta-expansion
Scala 3 expands a method to a function automatically (automatic eta-expansion), which makes the trailing m _ syntax
unnecessary. It also stops turning a plain value into a nullary function:
//> using scala 3.10.0
def twice(n: Int): Int = n * 2
@main def etaDemo(): Unit =
val x = 1
val f: () => Int = x _
val g = twice _
println((f(), g(4)))
-- [E099] Syntax Error: eta.scala:7:23
7 | val f: () => Int = x _
| ^^^
|Only function types can be followed by _ but the current expression has type Int
-- Warning: eta.scala:8:16
8 | val g = twice _
| ^^^^^^^
|The trailing ` _` for eta-expansion is unnecessary;
|the compiler performs eta-expansion automatically, so you can write the function value directly instead.
|This construct can be rewritten automatically under -rewrite -source 3.4-migration.
1 warning found
1 error found
The two lines differ in severity. x _ on a value is an error; the rewrite replaces it with (() => x). twice _ on a
method is only a warning since Scala 3.4 (write plain twice). The migration guide lists the row as a Scala 2.13
deprecation, but 2.13.18 itself rejects x _ on an Int with _ must follow method; cannot follow Int, so in practice
the row concerns method references.
any2stringadd
Scala 2 had an implicit conversion Predef.any2stringadd that added + to every type, so new AnyRef + "foo" compiled.
It is deprecated in 2.13 and removed in Scala 3.
-- [E008] Not Found Error: any2.scala:4:23
4 | val str = new AnyRef + "foo"
| ^^^^^^^^^^^^
| value + is not a member of Object
1 error found
The replacement is an explicit conversion, String.valueOf(x) + "foo", or string interpolation (see
Strings & Interpolation). The Scalafix rule
fix.scala213.Any2StringAdd from scala/scala-rewrites applies it on 2.13.
Early initializers
extends { val name = "Foo" } with Bar initialized fields before the parent trait’s body. They were rarely used and
mostly compensated for the lack of trait parameters, which Scala 3 now has.
//> using scala 3.10.0
trait Bar:
val name: String
val size: Int = name.size
object Foo extends {
val name = "Foo"
} with Bar
-- Error: early.scala:7:19
7 |object Foo extends {
| ^
| `extends` must be followed by at least one parent
-- [E009] Syntax Error: early.scala:9:2
9 |} with Bar
| ^^^^
| Early definitions are not supported; use trait parameters instead
...
3 errors found
The Scala 3 form passes the value as a trait parameter:
//> using scala 3.10.0
trait Bar(val name: String):
val size: Int = name.size
object Foo extends Bar("Foo")
@main def earlyFix(): Unit =
println(Foo.size)
prints 3. A trait parameter is not available in 2.13, so code that cross-compiles needs an intermediate class
(abstract class BarEarlyInit(val name: String) extends Bar) or a secondary constructor. Early initializers were also used
to give a subclass private state that the superclass constructor reads through an overridden method; the migration guide
moves that state into a nested private object state, which is initialized on demand. Trait parameters are covered in
Traits & Mixins.
Existential types
T forSome { type T } is dropped. The reference gives three reasons: it violates the soundness principle on which DOT
(the calculus under Scala 3) is built, it interacts badly with other features, and it overlaps path-dependent types.
-- [E034] Syntax Error: exist.scala:3:24
3 |def foo: List[Class[T]] forSome { type T } = Nil
| ^^^^^^^
| Existential types are no longer supported -
| use a wildcard or dependent type instead
...
3 errors found
Wildcard-only existentials remain legal and are treated as refined types, so List[?] replaces
List[T] forSome { type T }. When a wildcard is not enough, wrap the type in an enclosing type that carries it as a member:
trait Bar:
type T
val value: List[Class[T]]
When the compiler reads a Scala 2 class file that contains an existential, it approximates it and warns that a precise
emulation is not possible. Wildcards are written ? since 3.4; _ still works but warns (see From warnings to errors: the 3.3 to 3.10 timeline).
@specialized
@specialized made the Scala 2 compiler generate primitive-typed copies of a generic class. Scala 3 ignores the
annotation (only Function and Tuple have limited specialization) and does not even warn:
//> using scala 3.10.0
class Box[@specialized(Int) A](val a: A)
@main def specDemo(): Unit =
println(Box(3).a)
prints 3. This is the one "silent" row of the table: the code compiles and runs, but the annotation has no effect, which
can matter for hot paths that relied on the specialized variants. Similar
benefits can come from inline methods, see Inline.
Contextual abstractions
The redesign of implicits (given, using, Conversion, extension) brought four well-defined incompatibilities, and
changed the implicit resolution rules, which can make an unchanged implicit parameter unresolvable. The new rules
are in Conversions, into & Implicit
Resolution; the new constructs in
Givens & Using Clauses.
Type of an implicit definition
Implicit `val`s and `def`s need an explicit result type in Scala 3; the type cannot be inferred from the right-hand side any more.
//> using scala 3.10.0
implicit val answer = 42
@main def implTypeDemo(): Unit =
println(summon[Int])
-- Error: impltype.scala:3:13
3 |implicit val answer = 42
| ^
| type of implicit definition needs to be given explicitly
-- [E172] Type Error: impltype.scala:6:21
6 | println(summon[Int])
| ^
|No given instance of type Int was found for parameter x of method summon in object Predef
2 errors found
Add the type (implicit val answer: Int = 42), or better, convert it to given answer: Int = 42. The Scalafix rule
ExplicitResultTypes writes the missing types on 2.13, and 2.13.18 already warns about the case with
Implicit definition should have explicit type (inferred Int).
Implicit views
Scala 3 does not turn an implicit function value parameter ev: A => B into an implicit conversion, so the body below
can no longer call print on an A.
//> using scala 3.10.0
trait Pretty:
val print: String
def pretty[A](a: A)(implicit ev: A => Pretty): String =
a.print
-- [E008] Not Found Error: views.scala:7:4
7 | a.print
| ^^^^^^^
| value print is not a member of A
1 error found
This is the first runtime incompatibility of the page. In the migration guide’s second example, where a broader
implicit def anyPretty(any: Any): Pretty is in scope, the code compiles in both languages, but under
-source:3.0-migration the compiler applies ev, while under the default -source:3.0 it silently applies
anyPretty, which prints any. The fix is to call the conversion explicitly, ev(a).print, or to turn the parameter into
a real conversion with using:
//> using scala 3.10.0
trait Pretty:
val print: String
given Conversion[Int, Pretty] = n => new Pretty { val print = s"<$n>" }
def pretty[A](a: A)(using ev: A => Pretty): String =
ev(a).print
@main def viewsFixDemo(): Unit =
println(pretty(7))
The output is <7>. The compiler’s migration mode warns about these sites but does not rewrite them.
View bounds
A <% B was shorthand for "an implicit conversion from A to B exists". It was deprecated for years and is removed.
-- Error: viewbound.scala:3:10
3 |def foo[A <% Long](a: A): Long = a
| ^
| view bounds `<%' are no longer supported, use a context bound `:' instead
-- [E007] Type Mismatch Error: viewbound.scala:3:33
...
2 errors found
The diagnostic suggests a context bound, but that would change the method’s signature. The binary-compatible fix keeps the same erased signature by writing the conversion out as an implicit parameter and calling it:
//> using scala 3.10.0
def foo[A](a: A)(using ev: A => Long): Long = ev(a)
@main def viewBoundFix(): Unit =
println(foo(5))
which prints 5 (an Int converts to Long by numeric widening). implicit ev is also fine on 2.13 and 3. Beware the
runtime incompatibility just described. A conversion that is not meant to be implicit should become an ordinary type
class, see Type Classes.
Ambiguous conversion on A and => A
When two implicit conversions exist, one from Boolean and one from => Boolean, Scala 2.13 preferred the by-value one.
Scala 3 reports an ambiguity.
//> using scala 3.10.0
import scala.language.implicitConversions
class Foo:
def foo(): String = "foo"
implicit def boolFoo(bool: Boolean): Foo = new Foo
implicit def lazyBoolFoo(lazyBool: => Boolean): Foo = new Foo
@main def ambigDemo(): Unit =
println(true.foo())
-- [E007] Type Mismatch Error: ambig.scala:11:10
11 | println(true.foo())
| ^^^^
|Found: (true : Boolean)
|Required: ?{ foo: ? }
|Note that implicit extension methods cannot be applied because they are ambiguous;
|both method boolFoo and method lazyBoolFoo provide an extension method `foo` on (true : Boolean)
1 error found
The temporary fix is to name the conversion, boolFoo(true).foo(), which prints foo. The lasting fix is to delete the
by-name overload or to replace both with one extension method.
Other changed features
Eight rows describe features that Scala 3 simplified or restricted "to make the language easier, safer or more consistent". They have no deprecation on 2.13, and only the first has a migration rewrite.
Inheritance shadowing
An identifier used inside a class can name a member of an enclosing scope or an inherited member of the same name. Scala 2 silently preferred the inherited one, which depends on the parent’s code. Scala 3 asks you to say which.
//> using scala 3.10.0
class A:
val x = 2
object B:
val x = 1
class C extends A:
println(x)
@main def shadow2Demo(): Unit = new B.C
-- [E049] Reference Error: shadow2.scala:9:12
9 | println(x)
| ^
| Reference to x is ambiguous.
| It is both defined in object B
| and inherited subsequently in class C
1 error found
The same error appears on 3.3.8 and 3.9.0. The rewrite writes println(this.x) (inherited) and B.x is the outer one.
Choose deliberately: the rewrite assumes the Scala 2 meaning.
Non-private constructor in a private class
A constructor declared private[foo] in a private class leaks the private class in its own type signature.
//> using scala 3.10.0
package foo
object Foo:
private class Bar private[foo] ()
-- Error: privctor.scala:6:19
6 | private class Bar private[foo] ()
| ^
| non-private constructor Bar in class Bar refers to private class Bar
| in its type signature (): foo.Foo.Bar
1 error found
Since the class is private, make the constructor private too. The migration guide says migration mode only warns
about it; there is no automatic rewrite.
Abstract override
Re-declaring an inherited concrete method as abstract in a subtrait did nothing in Scala 2; in Scala 3 the subtrait’s version wins and subclasses must implement it.
//> using scala 3.10.0
trait A:
def bar(x: Int): Int = x + 3
trait B extends A:
def bar(x: Int): Int
class C extends B
-- [E231] Declaration Error: absover.scala:9:6
9 |class C extends B
| ^
|class C needs to be abstract, since def bar(x: Int): Int in trait B is not defined
1 error found
Delete the abstract def in B; it had no effect on 2.13. The decision is recorded in Dotty issue 4770.
Case class companion
In Scala 2 the companion object of a case class extended FunctionN, so Foo.tupled, Foo.curried and
Foo.andThen existed. Scala 3 companions do not extend those traits.
//> using scala 3.10.0
case class Foo(x: Int, b: Boolean)
@main def ccompDemo(): Unit =
println(Foo.curried(1)(true))
println(Foo.tupled((2, false)))
-- [E008] Not Found Error: ccomp.scala:6:14
6 | println(Foo.curried(1)(true))
| ^^^^^^^^^^^
| value curried is not a member of object Foo
-- [E008] Not Found Error: ccomp.scala:7:14
7 | println(Foo.tupled((2, false)))
| ^^^^^^^^^^
| value tupled is not a member of object Foo
2 errors found
Give the constructor a function type explicitly:
//> using scala 3.10.0
case class Foo(x: Int, b: Boolean)
@main def ccompFixDemo(): Unit =
val fooCtr: (Int, Boolean) => Foo = Foo.apply
println(fooCtr.curried(1)(true))
println(fooCtr.tupled((2, false)))
Output: Foo(1,true) and Foo(2,false). The guide’s alternative (Foo.apply _).curried(1)(true) also cross-compiles but
now carries the trailing-underscore warning. A named function value allocates once, which helps hot paths.
See Case Classes.
Explicit call to unapply
The synthetic unapply of a case class returned Option[(A, B)] in Scala 2; Scala 3 returns the case class itself
(the extractor is option-less, which avoids an allocation). Pattern matching is unaffected, and so are user-written
extractors. Only a direct call changes:
//> using scala 3.10.0
case class Location(lat: Double, long: Double)
def tuple(location: Location): (Double, Double) =
Location.unapply(location).get
-- [E008] Not Found Error: unap.scala:6:29
6 | Location.unapply(location).get
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
| value get is not a member of Location
1 error found
A pattern binding replaces the call:
//> using scala 3.10.0
case class Location(lat: Double, long: Double)
def tuple(location: Location): (Double, Double) =
val Location(lat, lon) = location
(lat, lon)
@main def unapFixDemo(): Unit =
println(tuple(Location(1.5, 2.5)))
println(Location.unapply(Location(3, 4)))
The output is (1.5,2.5) and Location(3.0,4.0), the second line showing that unapply now returns its argument. More
on extractors in
Extractors & Pattern Bindings.
Invisible bean property
The getter and setter that @BeanProperty generates exist in the bytecode (for Java frameworks) but are invisible to
Scala 3 source.
-- [E008] Not Found Error: bean.scala:9:7
9 | pojo.setFooBar("hello")
| ^^^^^^^^^^^^^^
| value setFooBar is not a member of Pojo
-- [E008] Not Found Error: bean.scala:10:15
10 | println(pojo.getFooBar())
| ^^^^^^^^^^^^^^
| value getFooBar is not a member of Pojo
2 errors found
Use the idiomatic accessor, pojo.fooBar = "hello". The accessors are still generated, which a reflection listing of the
class confirms:
//> using scala 3.10.0
import scala.beans.BeanProperty
class Pojo():
@BeanProperty var fooBar: String = ""
@main def beanFixDemo(): Unit =
val pojo = new Pojo()
pojo.fooBar = "hello"
println(pojo.fooBar)
val names = classOf[Pojo].getMethods.map(_.getName).filter(_.endsWith("FooBar")).sorted
println(names.mkString(", "))
prints hello and getFooBar, setFooBar. Java code that uses the JavaBeans naming sees them as before; see
Calling Scala from Java.
=> T as a type argument
A by-name type cannot instantiate a type parameter. The standard library function Function.uncurried has such a case:
//> using scala 3.10.0
val f: Int => (=> Int) => Int = a => b => a + b
val g: (Int, => Int) => Int = Function.uncurried(f)
-- [E134] Type Error: byname.scala:4:39
4 |val g: (Int, => Int) => Int = Function.uncurried(f)
| ^^^^^^^^^^^^^^^^^^
|None of the overloaded alternatives of method uncurried in object Function with types
| [T1, T2, T3, T4, T5, R]
| (f: T1 => T2 => T3 => T4 => T5 => R): (T1, T2, T3, T4, T5) => R
...
|match arguments ((f² : Int => (=> Int) => Int))
1 error found
The fix depends on the situation: write your own uncurried with the by-name signature, or inline its two-line body.
Wildcard type argument on a higher-kinded member
Scala 3 cannot reduce the application of an abstract higher-kinded type member to a wildcard.
//> using scala 3.10.0
trait Example:
type Foo[A]
def f(foo: Foo[_]): Unit
-- Warning: wild.scala:5:17
5 | def f(foo: Foo[_]): Unit
| ^
|`_` is deprecated for wildcard arguments of types: use `?` instead
|This construct can be rewritten automatically under -rewrite -source 3.4-migration.
-- [E043] Type Error: wild.scala:5:13
5 | def f(foo: Foo[_]): Unit
| ^^^^^^
|unreducible application of higher-kinded type Example.this.Foo to wildcard arguments
1 warning found
1 error found
Both messages appear: the _ deprecation of 3.4 and the real error. Replace the wildcard with a type parameter
(def f[A](foo: Foo[A])). When the member is itself an argument, as in Seq[Foo[_]], wrap it:
class FooWrapper[A](foo: Foo[A]) and use Seq[FooWrapper[?]]. That fixed version compiles on 3.10:
//> using scala 3.10.0
trait Example:
type Foo[A]
def f[A](foo: Foo[A]): Unit
class FooWrapper[A](foo: Foo[A])
def g(foos: Seq[FooWrapper[?]]): Unit
Type checker
The Scala 2.13 type checker is unsound in a few specific cases that Scala 3 fixes, so some code that compiled and crashed at run time now fails at compile time.
Variance checks on default arguments and inner classes
Scala 2 did not check default arguments and inner classes for variance.
//> using scala 3.10.0
class Foo[-A](x: List[A]):
def f[B](y: List[B] = x): Unit = ???
class Outer[+A](x: A):
class Inner(y: A)
-- Error: variance.scala:4:24
4 | def f[B](y: List[B] = x): Unit = ???
| ^
|contravariant type A occurs in covariant position in type [B]: List[A] of method f$default$1
-- Error: variance.scala:7:14
7 | class Inner(y: A)
| ^^^^
|covariant type A occurs in contravariant position in type A of parameter y
2 errors found
Each case needs care. The usual options are to make A invariant, to add a bound on B, or to add an overload instead
of a default. Applying two of them:
//> using scala 3.10.0
class Foo[-A](x: List[A]):
def f[B](y: List[B]): Unit = println(y)
def f(): Unit = f(x)
class Outer[+A](x: A):
class Inner[B >: A](y: B):
def get: B = y
@main def varianceFixDemo(): Unit =
Foo[Int](List(1, 2)).f()
val o = Outer("a")
println(o.Inner("b").get)
It prints List(1, 2) and b. As a stop-gap, @uncheckedVariance silences the check, but the unsoundness stays. Variance
itself is the subject of Variance.
Pattern matching soundness
A pattern on two values can bind them to different type arguments of the same generic class. Scala 3 refuses to treat them as the same.
//> using scala 3.10.0
trait Request
case class Fetch[A](ids: Set[A]) extends Request
object Request:
def combineFetch[A](x: Fetch[A], y: Fetch[A]): Fetch[A] = Fetch(x.ids ++ y.ids)
def combineReq(x: Request, y: Request): Request =
(x, y) match
case (x @ Fetch(_), y @ Fetch(_)) => combineFetch(x, y)
-- [E007] Type Mismatch Error: patm.scala:11:59
11 | case (x @ Fetch(_), y @ Fetch(_)) => combineFetch(x, y)
| ^
| Found: (y : Fetch[A$2])
| Required: Fetch[A$1]
1 error found
The compiler is right: nothing proves both Fetch values share a type argument. Relax the signature so that the
argument types may differ as long as they have a common ancestor:
def combineFetch[A](x: Fetch[? <: A], y: Fetch[? <: A]): Fetch[A] = Fetch(x.ids ++ y.ids)
With that change, combineReq(Fetch(Set(1)), Fetch(Set("a"))) returns Fetch(Set(1, a)) (a Set[Int | String]), the
case the old signature could never type-check. A cast would also compile but only moves the failure to run time.
Type inference
Scala 3 redesigned type inference, so a few programs infer a different type and a few get a new error. The guide
recommends writing the result types of all public members; the Scalafix rule ExplicitResultTypes can do so before the
migration.
Return type of an override
When an overriding method has no declared type, Scala 2.13 infers it from the right-hand side. Scala 3 takes it from the overridden method.
//> using scala 3.10.0
class Foo
class RichFoo(foo: Foo) extends Foo:
def show: String = "rich"
class Parent:
def foo: Foo = new Foo
class Child extends Parent:
override def foo = new RichFoo(super.foo)
@main def overRetDemo(): Unit =
println((new Child).foo.show)
-- [E008] Not Found Error: overret.scala:15:26
15 | println((new Child).foo.show)
| ^^^^^^^^^^^^^^^^^^^^
| value show is not a member of Foo
1 error found
Child#foo returns RichFoo on 2.13 and Foo on 3. Declaring override def foo: RichFoo keeps the 2.13 meaning, and the
program then prints rich. This is the second quiet change that can be a runtime difference (implicit conversions and
casts could pick another path), so public overrides should always state their type.
Reflective type
Scala 2 inferred a structural type for new { def bar = ... } and allowed foo.bar through reflection (with
import scala.language.reflectiveCalls). Scala 3 infers Object for the anonymous class.
//> using scala 3.10.0
import scala.language.reflectiveCalls
val foo = new { def bar: Int = 7 }
@main def reflDemo(): Unit =
println(foo.bar)
-- [E008] Not Found Error: refl.scala:7:14
7 | println(foo.bar)
| ^^^^^^^
| value bar is not a member of Object
1 error found
The fix is to write the structural type: val foo: { def bar: Int } = new { def bar: Int = 7 }. The program then prints 7,
but with -deprecation the call produces a warning: method reflectiveSelectableFromLangReflectiveCalls in object
Selectable is deprecated since 3.0: import scala.reflect.Selectable.reflectiveSelectable instead of
scala.language.reflectiveCalls. Import scala.reflect.Selectable.reflectiveSelectable instead. Structural types,
Selectable and Dynamic have their own page,
Structural Types, Selectable & Dynamic.
Macros
The Scala 3 compiler cannot expand Scala 2.13 macros. A macro definition compiles to nothing it can use:
//> using scala 3.10.0
//> using dep org.scala-lang:scala-reflect:2.13.18
import scala.language.experimental.macros
import scala.reflect.macros.blackbox.Context
object M:
def hello: String = macro helloImpl
def helloImpl(c: Context): c.Expr[String] = ???
-- Error: macro2.scala:7:6
7 | def hello: String = macro helloImpl
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
| No Scala 3 implementation found for this Scala 2 macro.
1 error found
The Scala 2 macro system, including def macros, quasiquotes, TypeTag and runtime reflection through scala-reflect,
is replaced by inline, scala.compiletime, quotes and splices, and quotes.reflect. A library may keep both
implementations side by side in one artifact (the Scala 3 compiler uses the Scala 3 one), a technique called mixing. It
serves Scala 2.13 users only if the artifact is built by Scala 3.7 or older, because Scala 2.13’s -Ytasty-reader
supports Scala 3 libraries up to 3.7 and not from 3.8 on. Mixing and its alternative, cross-building one artifact per
Scala version, are described in
Migrating from Scala 2. The replacement features are
introduced in Metaprogramming Overview and
Macros: Quotes & Splices.
The reference’s dropped features
The language reference has a chapter of 17 dropped features. Eight overlap with the migration table; the table below maps all of them, and the sections after it cover the nine that the table does not.
| Reference page | Status on 3.10 | Where on this page |
|---|---|---|
Dropped: DelayedInit |
semantics gone, |
|
Dropped: Scala 2 Macros |
error |
|
Dropped: Existential Types |
error |
|
Dropped: General Type Projection |
error on abstract types |
|
Dropped: Do-While |
error |
|
Dropped: Procedure Syntax |
error |
|
Dropped: Package Objects |
still compile, no warning |
|
Dropped: Early Initializers |
error |
|
Dropped: Class Shadowing |
error |
|
Dropped: Limit 22 |
lifted |
|
Dropped: XML Literals |
compile only with |
|
Dropped: Symbol Literals |
error |
|
Dropped: Auto-Application |
error |
|
Dropped: Weak Conformance |
gone |
|
Deprecated: Nonlocal Returns |
warning |
|
Dropped: |
warning |
|
Dropped: Wildcard Initializer |
warning |
DelayedInit and App
The special handling of DelayedInit is gone. DelayedInit still exists and is deprecated since 2.11.0, but a class
that extends it no longer has its constructor body passed to delayedInit:
//> using scala 3.10.0
trait Logger extends DelayedInit:
def delayedInit(body: => Unit): Unit =
println("before")
body
class Job extends Logger:
println("running")
@main def delayedDemo(): Unit =
Job()
It compiles with a deprecation warning, and prints only running; in Scala 2 it printed before first. The consequence
that surprises people is App, which was built on DelayedInit. The reference says it is "partially broken", and on
3.10 App is itself deprecated, since 3.8.0:
//> using scala 3.10.0
object Greeter extends App:
println(s"args = ${args.length}")
-- Deprecation Warning: app.scala:3:23
3 |object Greeter extends App:
| ^^^
|trait App in package scala is deprecated since 3.8.0: Support for trait App is deprecated in Scala 3. Please refer to https://docs.scala-lang.org/scala3/book/methods-main-methods.html.
1 warning found
Exception in thread "main" java.lang.ExceptionInInitializerError
at Greeter.main(app.scala)
Caused by: java.lang.NullPointerException: Cannot read the array length because the return value of "Greeter$.args()" is null
at Greeter$.<clinit>(app.scala:4)
Two real consequences: the body runs in the object’s static initializer (on some JVMs it is only interpreted, so never
benchmark inside an App), and args is null there, so reading it crashes. An App that never reads args still runs
(a variant printing started ran fine). The replacements are @main or an explicit main:
//> using scala 3.10.0
@main def greet(names: String*): Unit =
println(s"${names.length} names: ${names.mkString(", ")}")
With the arguments ada grace this prints 2 names: ada, grace. An object Hello { def main(args: Array[String]) } run
with three arguments prints args = 3. See
Hello World & @main Methods.
General type projection
T#A is only legal when T is a concrete type. On an abstract type, type parameter or alias to one, it is unsound
(combined with other Scala 3 features) and it rules out the type-level encoding of a combinator calculus.
//> using scala 3.10.0
trait Holder:
type A
def get[T <: Holder](a: T#A): T#A = a
object Allowed:
class Outer:
class In
def ok(x: Outer#In): Outer#In = x
-- Error: tproj.scala:6:24
6 |def get[T <: Holder](a: T#A): T#A = a
| ^
| T is not a legal path
| since it is not a concrete type
-- Error: tproj.scala:6:30
...
2 errors found
The Outer#In projection on the concrete class compiles. For abstract prefixes, use a path-dependent type
(def get(h: Holder)(a: h.A): h.A) or an implicit parameter. The same rule explains why type lambdas written with the
Scala 2 projection trick are unnecessary now: see Type Lambdas & Higher Kinds.
Package objects
The reference page says package objects "will be dropped", but they are only slated: they still work on 3.10, without
a warning even under -deprecation.
//> using scala 3.10.0
package object trailhead:
val maxGrade = 6
package trailhead:
def describe: String = s"max grade $maxGrade"
@main def pkgObjDemo(): Unit =
println(trailhead.describe)
prints max grade 6. The Scala 3 form is a top-level definition, in any file of the package. The compiler wraps top-level
definitions in a synthetic object named after the file (src$package), so renaming such a file changes the binary name of
what it contains. The 3.10 release notes say there is no concrete plan to remove package objects, and SIP-68, which lets
referenceable package objects behave regularly, is a preview feature in 3.10. See
Packages, Imports & Top-Level Definitions.
Class shadowing
An inner class cannot have the same name as an inner class of its parent. In Scala 2 the two looked like an override but classes cannot be overridden.
//> using scala 3.10.0
class Base:
class Ops
class Sub extends Base:
class Ops
-- [E069] Naming Error: shadowcls.scala:7:8
7 | class Ops
| ^
|class Ops cannot have the same name as class Ops in class Base -- class definitions cannot be overridden
1 error found
Rename one of the classes.
The limit of 22
Function types and tuples are no longer limited to 22 parameters or fields. Beyond 22 they erase to
scala.runtime.FunctionXXL and scala.runtime.TupleXXL, implemented with arrays. A 23-field tuple and a 23-parameter
function, generated by script, compile and run:
//> using scala 3.10.0
@main def limitDemo(): Unit =
val big = (1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23)
println(big.size)
println(big(22))
val sum: (Int, Int, Int, Int, Int, Int, Int, Int, Int, Int, Int, Int, Int, Int, Int, Int, Int, Int, Int, Int, Int, Int, Int) => Int =
(a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13, a14, a15, a16, a17, a18, a19, a20, a21, a22, a23) =>
a1 + a2 + a3 + a4 + a5 + a6 + a7 + a8 + a9 + a10 + a11 + a12 + a13 + a14 + a15 + a16 + a17 + a18 + a19 + a20 + a21 + a22 + a23
println(sum(1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1))
The output is 23, 23 and 23. This is a removal of a limit, not an incompatibility, but code that worked around it with nested tuples can now be
flattened. See Tuples & Named Tuples.
XML literals
Inline XML (<a>b</a>) in Scala source is on its way out: the reference says literals "will be dropped in the near
future" in favor of string interpolation. On 3.10 they still parse, but the compiler needs the scala-xml library and
reports a missing package otherwise:
-- [E008] Not Found Error: xmlnodep.scala:4:10
4 | val x = <a>b</a>
| ^
| value xml is not a member of scala
1 error found
With //> using dep org.scala-lang.modules::scala-xml:2.4.0 the program
val x = <peak><name>{name}</name></peak> prints <peak><name>Pico</name></peak>. The hint printed by Scala CLI at
that point recommended 2.5.0, which was the newest version on Maven Central at the check date. New code should build
documents with scala.xml constructors, a template library or the string interpolators of an XML library.
Weak conformance
Scala 2 used weak conformance to widen numeric types, so List(n, c, d) with an Int, a Char and a Double was a
List[Double]. Scala 3 keeps one rule: an Int literal adapts to another numeric type when needed.
//> using scala 3.10.0
@main def weakDemo(): Unit =
val n: Int = 3
val c: Char = 'X'
val d: Double = math.sqrt(3.0)
val literals: List[Double] = List(1.0, math.sqrt(4.0), 0, -3.3)
val mixed = List(n, c, d)
val asDoubles: List[Double] = mixed
println(literals)
-- [E007] Type Mismatch Error: weak.scala:9:32
9 | val asDoubles: List[Double] = mixed
| ^^^^^
| Found: (mixed : List[Int | Char | Double])
| Required: List[Double]
1 error found
Without the last assignment the program prints List(1.0, 2.0, 0.0, -3.3): the literal 0 adapts to Double. On 3.10 the
type of the mixed list is the union List[Int | Char | Double], not even List[AnyVal] as the reference says (the exact
printed type is more precise than the documentation). Add a type ascription or convert the values (n.toDouble).
Nonlocal returns
A return inside a lambda used to unwind through an exception, NonLocalReturnException, with a hidden cost and a leaky
failure mode (a catch of Exception would intercept it). It is deprecated since 3.2.0 and warns on 3.10 without any flag:
//> using scala 3.10.0
def firstIndex[T](xs: List[T], elem: T): Int =
xs.zipWithIndex.foreach { (x, i) =>
if x == elem then return i
}
-1
@main def nonlocalDemo(): Unit =
println(firstIndex(List("a", "b", "c"), "b"))
-- Warning: nonlocal.scala:5:22
5 | if x == elem then return i
| ^^^^^^^^
|Non local returns are no longer supported; use `boundary` and `boundary.break` in `scala.util` instead
1 warning found
1
The replacement is scala.util.boundary, which also replaces the Breaks API. It is explicit, lexically scoped and
checked by the compiler:
//> using scala 3.10.0
import scala.util.boundary, boundary.break
def firstIndex[T](xs: List[T], elem: T): Int =
boundary:
for (x, i) <- xs.zipWithIndex do
if x == elem then break(i)
-1
@main def boundaryDemo(): Unit =
println(firstIndex(List("a", "b", "c"), "b"))
println(firstIndex(List("a", "b", "c"), "z"))
The output is 1 and -1. See
Exceptions, boundary & Early Exit.
private[this] and protected[this]
The compiler now infers that a private member only accessed through this is object-private, which was the purpose of
private[this]. The qualified form is deprecated, and protected[this] is dropped without replacement. On 3.10 the old
form compiles with a warning:
-- Warning: privthis.scala:4:9
4 | private[this] var n = 0
| ^^^^^^
|Ignoring [this] qualifier.
|The syntax `private[this]` will be deprecated in the future; just write `private` instead.
|See: https://docs.scala-lang.org/scala3/reference/dropped-features/this-qualifier.html
|This construct can be rewritten automatically under -rewrite -source 3.4-migration.
1 warning found
The rewrite drops the qualifier. The subtle part is the second runtime incompatibility: a private val that is only used
through this and by no method may no longer generate a field, so reflection that expected the field fails. The fix is
the qualified form with the class as qualifier, private[C] val field = x + 1. Class parameters declared private val
keep their fields and variance checks.
Source: Scala 3 Reference: Dropped: private[this and protected[this]]
Wildcard initializer
var x: A = _ declared an uninitialized field. The marker is now a value in scala.compiletime:
//> using scala 3.10.0
import scala.compiletime.uninitialized
class Cell:
var x: Int = uninitialized
var name: String = uninitialized
@main def wildInitFixDemo(): Unit =
val c = Cell()
println((c.x, c.name))
The output is (0,null). The old = _ compiles with the warning
= _ has been deprecated; use = uninitialized instead and is rewritten by -source:3.4-migration.
From warnings to errors: the 3.3 to 3.10 timeline
Scala 3.0 to 3.3 kept several Scala 2 forms alive for migration. From 3.4 they warned, and 3.10 removed the first of them. So an old Scala 3 source can also fail on 3.10. The table gives the severity of each form on the three compilers tried here; every cell is a measured result, not documentation.
| Form | 3.3.8 | 3.9.0 | 3.10.0 | Replacement |
|---|---|---|---|---|
|
silent |
warning |
error |
|
|
silent |
warning |
warning |
|
|
silent |
warning |
warning |
|
|
silent |
warning |
warning |
|
|
silent |
warning |
warning |
|
trailing |
silent |
warning |
warning |
|
|
warning |
error |
error |
|
|
silent |
silent |
error |
remove it |
given in an inaccessible companion |
silent |
warning with |
not found |
public companion |
|
custom |
custom |
|
|
|
not run |
not run |
deprecated |
extension methods |
The intermediate releases 3.4 to 3.8 were not run; the 3.9 release notes say that -source:3.4-migration -rewrite
applies every one of the 3.4 rewrites.
A with B becomes an error
The Scala 2 compound type has been deprecated in favor of & since 3.4. On 3.10:
-- [E003] Syntax Error: with2.scala:5:15
5 |def f(x: Named with Sized): Unit = ()
| ^^^^
|with as a type operator has been deprecated; use & instead
|This construct can be rewritten automatically under -rewrite -source 3.4-migration.
-- [E003] Syntax Error: with2.scala:7:17
7 | case y: (Named with Sized) => ()
...
2 errors found
The identical file on 3.9.0 reports two warnings under the same code E003, and 3.3.8 compiles silently. In a typed pattern
the intersection needs parentheses, case y: (A & B); the rewrite adds them. with remains valid in class C extends A with B
(a different construct). The 3.10 release notes name this among the removed legacy features that end cross-compilation between
Scala 2 and 3.10.
The 3.4 deprecations in one rewrite
The remaining items of the 3.4 list (_ type wildcards, xs: _*, private[this], var x = _ and the trailing _) are
all fixed at once by -source:3.4-migration -rewrite. Applied to a class with those forms it produces exactly:
- private[this] var n = 0
+ private var n = 0
- var cache: Int = _
+ var cache: Int = scala.compiletime.uninitialized
- def label(x: Named with Sized): String = "x"
+ def label(x: Named & Sized): String = "x"
- def go(): Int = { val ys: List[_] = Nil; sum(List(1, 2): _*) + ys.size }
+ def go(): Int = { val ys: List[?] = Nil; sum(List(1, 2)*) + ys.size }
- val g = twice _
+ val g = twice
The compiler reported six warnings and patched the file. A later flag does not replay an earlier one, so the migration
to a newer LTS is a sequence of -source:3.4-migration, 3.5-migration and so on. That recipe is on
Upgrading Between Scala 3 Versions.
Refutable patterns in for generators
A generator pattern that is narrower than the type of the source needs an explicit case, because an implicit filter hid
data loss:
-- Error: refut.scala:5:14
5 | val a = for Some(x) <- xs yield x
| ^^^^^^^
|pattern's type Some[Int] is more specialized than the right hand side expression's type Option[Int]
|
|If the narrowing is intentional, this can be communicated by adding the `case` keyword before the full pattern,
|which will result in a filtering for expression (using `withFilter`).
|This patch can be rewritten automatically under -rewrite -source 3.2-migration.
1 error found
The same line is only a warning on 3.3.8 and is an error on 3.9.0 and 3.10.0. for case Some(x) <- xs yield x returns
List(1, 3). The mechanics of generators are in
For Comprehensions.
Source: Scala 3.9 release notes
Givens in an inaccessible companion
When the compiler looks for an implicit in a type’s companion it now only considers instances accessible at the call site.
//> using scala 3.10.0
object api:
class Config
private object Config:
given Config = new Config
def describe(using Config): String = "configured"
@main def privCompDemo(): Unit =
println(api.describe)
-- [E172] Type Error: privcomp.scala:10:22
10 | println(api.describe)
| ^
|No given instance of type api.Config was found for parameter x$1 of method describe in object api
1 error found
3.3.8 prints configured silently; 3.9.0 prints it with a deprecation warning under -deprecation:
Usage of implicit given instance given_Config defined in object Config, which is not accessible here. In Scala 3.10,
this implicit will no longer be found. The library-side fix is a public companion or a given in a public object. Users
usually cannot work around it, so publish the fix before your consumers move to 3.10.
this in a top-level definition
Before 3.10, val self = this at the top of a file resolved to the synthetic package object of the file. That was almost
never intended:
-- Error: topthis.scala:3:11
3 |val self = this
| ^^^^
| Top-level definitions cannot refer to this
1 error found
It compiles on 3.9.0 and 3.3.8. Remove the reference or move the definition into an object.
-> is now an extension method
The standard a -> b pair syntax became an inline extension method on 3.10, and Predef.ArrowAssoc stopped being an
implicit conversion. That can silently change which -> runs. A DSL that defines -> through an implicit class loses to
the standard one:
//> using scala 3.10.0
object dsl:
implicit class Arrow[A](a: A):
def ->[B](b: B): String = s"custom($a, $b)"
import dsl.*
@main def arrow2Demo(): Unit =
println(1 -> "a")
On 3.10.0 this prints the tuple (1,a). The same file prints custom(1, a) on 3.9.0 and 3.3.8. No error, no warning: the
program compiles and the meaning changes. The version defined as an extension method keeps its priority and prints
custom(1, a) on 3.10 when dsl.* is imported. So a DSL that ships its own -> has to be rewritten as an extension. See
Domain Specific Languages.
Using the old names directly produces deprecation warnings, both since 3.10.0:
class RichInt in package scala.runtime is deprecated since 3.10.0: use the extension methods available on primitive types instead
method ArrowAssoc in object Predef is deprecated since 3.10.0: Use `->` extension method instead.
Ordinary uses (1 to 3, 3.max(4), 5.abs) are unaffected. Macros and compiler plugins that inspect the old Rich*
trees must match the new shape.
Strictness that is still opt-in
Two checks that Scala 3 introduced are not errors by default on 3.10. An alphanumeric method that is not declared infix
and is used as an infix operator warns by default since 3.4 (3.3.8 accepts it silently), and the warning becomes an error
only under -source:future. The Matchable constraint on a type test of an Any value is silent by default and warns
only under -source:future. The program:
//> using scala 3.10.0
case class Pair(n: Int):
def plus(m: Int): Int = n + m
@main def run(): Unit =
val x: Any = 3
println(Pair(1) plus 2)
x match
case s: String => println(s)
case i: Int => println(i)
Compiled as it is, it prints 3 twice after one warning:
-- Warning: infix.scala:8:18
8 | println(Pair(1) plus 2)
| ^^^^
|Alphanumeric method plus is not declared infix; it should not be used as infix operator.
|Instead, use method syntax .plus(...) or backticked identifier `plus`.
|The latter can be rewritten automatically under -rewrite -source 3.4-migration.
1 warning found
With //> using options -source:future the same use is an error, and both type tests warn:
-- Error: infix.scala:8:18
8 | println(Pair(1) plus 2)
| ^^^^
|Alphanumeric method plus is not declared infix; it should not be used as infix operator.
|Instead, use method syntax .plus(...) or backticked identifier `plus`.
|The latter can be rewritten automatically under -rewrite -source 3.4-migration.
-- [E165] Type Warning: infix.scala:10:12
10 | case s: String => println(s)
| ^^^^^^
| pattern selector should be an instance of Matchable,
| but it has unmatchable type Any instead
-- [E165] Type Warning: infix.scala:11:12
11 | case i: Int => println(i)
| ^^^
| pattern selector should be an instance of Matchable,
| but it has unmatchable type Any instead
2 warnings found
1 error found
Declare such methods infix and keep values in a Matchable-typed variable, to be ready for the day these become the
default. The infix warning is also covered, with the style guide’s view of it, on
Style Guide. See
Type Hierarchy & Matchable.
Related forms that still work
Three Scala 2 forms look like candidates for removal but behave as follows on 3.10:
-
Type lambdas written with a projection compile:
lift[({ type L[x] = List[x] })#L](List(1))is accepted, because the prefix is a concrete refinement. The Scala 3 form is[x] =>> List[x]. The kind-projector syntaxEither[String, *]is not accepted by default (Not found: type *); it needs-Xkind-projector, which then printsRight(1)forlift[Either[String, *]](Right(1))andRight(2)forlift[[x] =>> Either[String, x]](Right(2)). -
Implicit conversions and implicit classes still compile and run.
implicit def intToString(i: Int): Stringfollowed byval s: String = 42prints42, with the feature warningDefinition of implicit conversion method intToString should be enabled by adding the import clause 'import scala.language.implicitConversions'. Implicit classes stay, too ("hi".shoutprintedHI!), thoughextensionis preferred. Only the resolution rules changed. -
Deprecated Scala 2.13 library names still compile with warnings:
Traversable(useIterable),Stream(useLazyList),CanBuildFrom(useBuildFrom),JavaConverters(usescala.jdk.CollectionConverters) and the strictMap.mapValues(use.view.mapValues(f), then.toMap). The replacement program runs and prints(6,List(1, 2, 3),Map(1 -> 3),List(1, 2)).
Type lambdas have their own page, Type Lambdas & Higher Kinds.
Where the books are outdated
The three books consulted for this section are Programming Scala 3rd edition (abbreviated PS3), Learn Scala Programming (LSP) and Professional Scala (PRO). Each was written before some of the changes above, and the table collects every outdated form with the current state on 3.10. Chapter references are the issue’s own mapping. The table contains no book text, only the forms and their current replacement.
| Book statement or form | Current Scala 3.10 |
|---|---|
|
|
|
|
|
|
View bounds |
Removed (View bounds). Use a |
PS3’s |
The 3.6 given syntax (SIP-64): |
Given resolution examples (PS3 ch. 5; LSP ch. 4) |
3.7 prefers the most general given; 3.10 no longer finds givens in inaccessible companions (Givens in an inaccessible companion). |
PS3 ch. 16 view-bound example written with |
Pre-3.0 Dotty syntax and an error in the book. It is |
PS3 predicts that legacy syntax warns or fails "in 3.1": |
Warnings arrived in 3.4 ( |
|
Standard since 3.3. On 3.10 the option only prints a deprecation: "`fewerBraces` is now standard, no language import is needed". |
|
|
|
Since 3.5 |
JDK 8+ with 11 or 15 recommended (PS3 ch. 1); JDK 8 (PRO); JDK 8-11 (LSP) |
Scala 3.8+ needs JDK 17+; the 3.3 LTS still runs on JDK 8. See Versions, Compatibility & JDKs. |
"The Scala 3 standard library is the 2.13 library" (PS3 ch. 1, 14) |
Same API, but since 3.8 it is compiled by Scala 3; the REPL is a separate artifact ( |
Polymorphic function types cannot take context bounds or |
They can in recent 3.x: |
Compound types |
|
Existential types |
Dropped (Existential types). Use |
|
|
Type lambdas |
|
Type projections |
Allowed only on concrete prefixes (General type projection). |
Procedure syntax |
All dropped (Procedure syntax, do-while, Symbol literals). Use |
|
|
|
The 2.13 hierarchy ( |
“Option` extends `Traversable” (PRO ch. 5) |
Wrong even in 2.12, where an implicit conversion applied. |
|
|
Scala 2 def macros, quasiquotes, |
|
Structural types work out of the box (PRO ch. 5) |
Reflective access needs |
Akka as the default actor and streams toolkit (LSP ch. 11-14; PRO ch. 7; PS3 ch. 19) |
Akka has been under the Business Source License since 2.7 (2022). The Apache-licensed fork is Apache Pekko; Cats Effect, ZIO and Ox are common alternatives. Ecosystem page only. |
|
Out of scope; named here so that readers of the book know these APIs changed. |
Lagom and ConductR (LSP ch. 15) |
End of life (Lagom 2024-07-01). Not documented. |
sbt 1.1-1.2, ENSIME, Ammonite, Mill under |
sbt 1.x and sbt 2.0; Metals or IntelliJ; Scala CLI; Mill under |
ScalaTest 3.0 |
ScalaTest 3.2 |
"Dotty" as the future compiler (PRO, LSP) |
Scala 3.0 shipped on 2021-05-14. |
SPP, the Scala Platform Process (PRO ch. 8) |
Defunct. Use SIPs and the Scala Toolkit. |
Comparing with other languages
Scala is not alone in removing forms after years of deprecation. The path Scala 3.4 to 3.10 followed, a warning that
names its replacement, a rewrite switch, and an error only a few minor releases later, can be compared with the
breaking changes that Groovy collects release by release in
Groovy: What’s New & Migrating from Older Versions.
A different kind of constraint is the Java floor: Scala 3.8 and later need JDK 17, and the Maven release setting and
toolchains that pin a build to a Java version are covered in
Java Build and Tooling. Calling Java from Scala is covered in
Using Java from Scala.
Pitfalls
-
A green 2.13 build is not a green Scala 3 build. Only about a third of the 32 rows have a 2.13 deprecation or feature warning; the rest (open brace, wrong indentation, inheritance shadowing, abstract override, case-class companions, variance checks and the type-checker fixes) fail only on Scala 3.
-
The rewrite assumes the Scala 2 meaning.
this.xfor inheritance shadowing,Symbol("abc")for a literal and() => xfor value eta-expansion all keep the old behavior, even where the old behavior was the bug. Review the diff. -
-source:3.0-migrationwill not run on brace-and-indent mixes. Rewriting a file that already uses Scala 3 optional braces together with Scala 2 constructs can fail with new syntax errors (it did here withdef print() {inside a bracelessobject); run the rewrite on the Scala 2 style file first and convert to braceless afterwards. -
Silent behavior changes need tests. Four changes do not produce an error: the inferred type of an override (Return type of an override), an implicit view resolved to a different conversion (Implicit views),
->through an implicit class (->is now an extension method) and aprivate[this]field that is not generated (private[this]andprotected[this]). -
Binary compatibility is separate from source compatibility. 3.10 keeps loading libraries compiled with older Scala 3 versions that used the removed features; only the source of the library being compiled has to change. A 3.10-compiled library cannot be used by a project on 3.9 (see Versions, Compatibility & JDKs).
-
Libraries that must stay cross-compilable with Scala 2 should stay on the 3.9 LTS. The 3.10 release notes expect cross-compiled libraries to remain on the LTS indefinitely, and recommend 3.9 LTS for the longest support window.
-
Check MiMa. After a migration, run MiMa 1.2.1 or newer against the previous artifact: the removed features can change generic signatures, and the 3.10 notes mention
IncompatibleSignatureProblemreports as an example.
What was run and what was not
All the [source,scala] programs marked with //> using scala 3.10.0 were compiled with Scala CLI 1.17.1 (using
scala run for the @main programs and scala compile where there is no entry point), and their diagnostics and outputs
are the ones printed above. The -rewrite experiments were run with --scalac-option -rewrite --scalac-option -source:3.0-migration
and -source:3.4-migration on copies of the sources. The version comparisons used 3.3.8 and 3.9.0 for with, the vararg
and wildcard forms, the refutable generator, the top-level this, the inaccessible companion given, the -> priority
and inheritance shadowing, and 2.13.18 for the legacy program. The scala-xml dependency (2.4.0) and the scala-reflect
dependency (2.13.18) were fetched from Maven Central.
Not run: the Scalafix rules (ExplicitResultTypes, Any2StringAdd and others), because they need sbt or the Scalafix CLI;
sbt-scala3-migrate; and the Scala 2 macro cross-building recipes. These are described from the migration guide and are
covered in Migrating from Scala 2. The claims about the
intermediate releases 3.4 to 3.8 come from the release notes and were not run on those compilers.
References
-
Scala 3 Reference: Dropped: private[this and protected[this]]