Scala 3: What’s Changed Since Scala 3.0.0
The Scala team continues to refine Scala 3, fixing bugs, refining existing features, and introducing some experimental features for Scala 3.1. Here are a few highlights.
Programming Scala, Third Edition is now available. It provides a comprehensive introduction to Scala 3 for experienced Scala developers, as well as a complete introduction to Scala for new Scala developers.
Scala 3.0.1
This patch release mostly fixes bugs and makes small refinements that don’t impact the book’s contents, with two exceptions I’ll discuss here.
Simplified given
Syntax
In Scala 3.0.0, if you want to declare a given
for a class with no refinement required (i.e., defining an abstract method), you have to use one of two, slightly awkward syntax options. For example, in the book’s code examples, the JSONBuilder.scala
example defines a trait ValidJSONValue
to enumerate allowed types for building JSON from data structures. Here is how the trait and given
instances are defined in the book, using Scala 3.0.0 syntax:
sealed trait ValidJSONValue[T <: Matchable]
given ValidJSONValue[Int] with {}
given ValidJSONValue[Double] with {}
given ValidJSONValue[String] with {}
given ValidJSONValue[Boolean] with {}
given ValidJSONValue[JSONObject] with {}
given ValidJSONValue[JSONArray] with {}
Having to write with {}
is inconvenient and non-obvious, too. You can also define given
instances using given ValidJSONValue[Int] = ValidJSONValue[Int]()
, for example, but it seems a bit counterintuitive that you have to write the type twice.
Scala 3.0.1 introduces this new, more concise alternative:
sealed trait ValidJSONValue[T <: Matchable]
given ValidJSONValue[Int]()
given ValidJSONValue[Double]()
given ValidJSONValue[String]()
given ValidJSONValue[Boolean]()
given ValidJSONValue[JSONObject]()
given ValidJSONValue[JSONArray]()
(The latest commits in the examples repo use this updated syntax.)
@experimental
annotation
Scala 3.0.1 introduces a new @experimental
annotation that is used to mark definitions for experimental features. They can be used in the same situations where language.experimental
can be used. As stated in the pull request,
A class is experimental if
- It is annotated with
@experimental
- It is a nested class of an experimental class. Annotation
@experimental
is inferred. - It extends an experimental class. An error is emitted if it does not have the annotation.
A member definition is experimental if
- It is annotated with
@experimental
- All overridden definitions are experimental
- Its owner is an experimental class
The annotation definition itself, class experimental
is also experimental.
Scala 3.0.2
The Scala 3.0.2 release continues with bug fixes and refinements. None of them impact the book, so I won’t discuss the details here. See the release notes for more information.
Scala 3.1.0-RC1
The first release candidate for Scala 3.1.0 came out a few days ago. As you might expect, more significant changes are coming in the 3.1 minor release. Here are a few of the notable changes.
Experimental Safer Exceptions
One of the great things about Scala and the cutting-edge language research that goes into it, is the way that old problems can be revisited with a fresh perspective. The experimental safer exceptions is a good example.
From the beginning, Java has supported checked exceptions, where a method can’t throw any so-called of the checked exceptions unless the method signature explicit states it might throw them with a throws
clause. The checked exception classes are all subclasses of Throwable
, other than RuntimeException
and its subclasses, and Error
and its subclasses.
This is great for communicating to users what might happen if something fails and a value can’t be returned. Unfortunately, as designed, it forces all methods that call such methods to either catch and handle those exceptions or add corresponding throws
clauses.
While you could argue that disciplined design would make this work, in practice most Java developers use subtypes of RuntimeException
for custom exceptions and often catch the checked exceptions, then wrap them in unchecked exceptions, so they throw something without any pain…
In Scala, Either[A,B]
and similar types are often returned, avoiding thrown exceptions altogether, to achieve the same goal of ensuring the method signature fully describes all possible outcomes (along with other benefits I won’t go into). However, sometimes throwing an exception is a good design choice. The new safer exceptions feature is designed for such situations, while still providing appropriate method signatures, but without the drawbacks of classic checked exceptions.
I won’t discuss all the details here. The documentation page is worth reading for all the details. However, here is the gist of how this feature works.
Using the documentation’s example, you first import language.experimental.saferExceptions
to enable this feature. Now suppose you have the following code:
val limit = 10e9
class LimitExceeded extends Exception
def f(x: Double): Double =
if x < limit then f(x) else throw LimitExceeded())
With the feature enabled, f
will fail to compile because The ability to throw exception LimitExceeded is missing.
The message will also provide suggestions for how to fix this, including the following change:
def f(x: Double): Double canThrow LimitExceeded =
if x < limit then f(x) else throw LimitExceeded())
The new canThrow
defines what exceptions can be thrown. The implementation will use compiler-generated given
instances of a type CanThrow[E]
. In this case, erased given ctl: CanThrow[LimitExceeded] = ???
The erased
keyword is another experimental feature that marks definitions for removal from the generated byte code. Using ???
as the definition doesn’t cause any problems, because this instance will never be used at runtime, so the ???
method will never be invoked! Hence, this given
instance is effectively a “marker” to allow the throw LimitExceeded
expression to compile.
The implementation also has the important benefit of solving the checked exception issue described above, namely that callers of this method do not need to declare types with canThrow
. So, for example:
@main def test(xs: Double*) =
try println(xs.map(f).sum)
catch case ex: LimitExceeded => println("too large")
Here, our old friend println
compiles just fine, as before, even though f
can throw an exception.
There are a lot more details in the documentation, including support for migrating code to incrementally to use this feature.
More Configurable Compiler Warnings
A “regression” from Scala 2 is the ability to mark code with a @nowarn
annotation, so the compiler doesn’t emit warnings for it and also the ability to configure warnings. Scala 3.1.0 will now support a -Wconf
option for configuring warnings and it restores the @nowarn
annotation.
More Type Class instances for CanEqual
Scala 3 introduces a new concept called multiversal equality that is implemented with a new CanEqual
type class. I haven’t blogged about this feature yet, but I’ll do that as my next post. I’ll mention a few improvements Scala 3.1 introduces, namely more type class instances (givens
) of CanEqual
for common types. Stay tuned.
See Programming Scala, Third Edition for more background information about most the features discussed in this post.