Scala 3: What’s Changed Since Scala 3.0.0

Dean Wampler
Scala 3
Published in
5 min readSep 4, 2021

--

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.

Green light, © 2021, Dean Wampler

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 giveninstances 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 -Wconfoption 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.

--

--

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.