Scala 3.2.0 released!

Monday 5 September 2022

Paweł Marks, VirtusLab

Scala 3.2

We are happy to announce the release of Scala 3.2.0. This new minor release allowed us to deliver some significant improvements.

What’s new in Scala 3.2.0

Support for code coverage

Scoverage is an excellent tool for keeping track of the test coverage of code written in Scala. Unfortunately, it relied on a compiler plugin to generate coverage data. This means that it was limited only to Scala 2 code. Now we have changed that. Since the launch of 3.2, the Scala compiler can natively generate coverage data. To enable that, use the -coverage-out flag and provide the output directory. The Scoverage plugin for sbt is even more convenient. It sets up the compiler flags, includes other parts of the Scoverage toolset, and automatically generates human-readable reports.

Exports in extension clauses

We have added Extension clauses to the list of places where users can export definitions. That allows, among other benefits, to add extensions method in bulk without relying on any form of implicit conversion. Let’s examine the simple example:

class RichInt(i: Int):
  def double = i * 2
  def triple = i * 3

extension (i: Int)
  private def richInt = RichInt(i)
  export richInt.*

This adds double and triple methods as extensions to the Int type, so calling 2.double will return 4.

In this example, there is no benefit in exporting the methods instead of declaring them as extensions. However, in real life, the equivalent of our RichInt class can be generic, rely on inheritance, declare its own exports from other types or be used in multiple extension clauses. This allows for much better reuse and modularity of extensions, which was not possible before.

Code completion for refined types

Scala 3.0 introduced a Selectable marker trait for types that support the dynamic selection of members. It allows bridging the outside world of untyped data (such as JSON) with strongly typed Scala code. In 3.2, the Selectable trait gained a huge boost as the compiler now generates code completion data for it based on type refinements. Metals and Scala REPL are already using said data and are providing code completion for Selectable’s members.

Let’s assume that in our code, we have some simple implementation of JSON data and the type for JSON representing a person. It may look something like this:

class Json(raw: String) extends Selectable:
  // provide the implementation for parsing here

type Person = Json {
  val name: String
  val age: Int
  val address: String
}

If we have an instance of the Person type named person and then type person.a in our IDE or REPL, the auto-completion will provide suggestions for the address and age fields.

If you are interested in how you can use Selectable and refined types for safer work with untyped data in your code, you may want to take a look at this example: Iskra. This simple library provides better type safety and auto-completion when working with Apache Spark’s data frames.

Improved Mirror synthesis

Mirrors are an important innovation in Scala 3. They are the given instances of type class generated by the compiler for selected types and, most importantly, all case classes and enums. They allow for defining type class derivation without using advanced metaprogramming techniques.

In 3.2, a lot of work was put into improving the Mirror synthesis. Most importantly, Mirrors are now generated for local and inner classes and generic tuples up to the size of 22. Also, there are substantial improvements in reporting Mirror-related errors. Now, the compiler prints a list of reasons why it couldn’t generate the requested Mirror.

Easier use of class constructors with using clauses

Until now, classes using clauses sometimes required passing awkward empty parameter lists to the constructor. For example, the following snippet:

class Example(using String)(i: Int)
given String = "something"
val instance = Example(6)

was failing with a misleading error message. The only proper way to instantiate the Example class was inserting an unintuitive empty parameter list and writing Example()(6).

Now the compiler correctly inserts parameter lists, so this snippet compiles as expected.

Given instances in for-comprehension

It is now possible to declare bindings in for-comprehensions as given instances that are visible in the for-comprehension scope.

def usesString(using String): List[Int] = ???

val ints: List[Int] =
  for
    given String <- List("abc", "def", "xyz") // this now works
    result <- usesString
  yield results

Statistics about code complexity

The new -Vprofile compiler flags give some insights into code complexity and where compilation time is likely spent. If set, the compiler prints a summary for every compiled source file. The summary contains:

  • the source file name,
  • the number of code lines in the source, excluding comments and whitespace,
  • the number of tokens (separately parsed words) in the source,
  • the number of TASTy chunks produced from the source. A chunk consists of 50 bytes of Tasty output that serializes the type-checked tree coming from the source, using some compression techniques. The size of the generated Tasty is a good estimator of compile times and code complexity.
  • the average complexity per line, computed by dividing the number of lines by the number of Tasty chunks.
  • an explanation of the average complexity number, computed as follows:

    • low: fewer Tasty chunks than source lines,
    • moderate: up to 5 times more TASTy chunks than source lines,
    • high: up to 25 times more TASTy chunks than source lines,
    • extreme: At least 25 times more TASTy chunks than source lines,
  • the directory where the source is located.

There are two additional related flags:

  • -Vprofile-sorted-by:<column label> prints the same output as -Vprofile, but it is sorted according to a specified column, for example: complexity or lines
  • -Vprofile-details <number> summarizes the profiles of the <number> most complex methods

The -Vprofile option family is useful for trouble-spotting long compile times.

New experimental APIs

The following new APIs were introduced to the language and currently can be used in experimental code. We are looking forward to user feedback before we decide to stabilise them.

Stabilised APIs

The 3.2 release stabilises a bunch of APIs that previously could be used only in experimental code. Those are:

Should I update to Scala 3.2.0?

We thoroughly tested all release candidates for 3.2.0 to detect any source incompatibilities. We run a so-called Open Community Build, a compilation of nearly 1000 real-life projects on each compiler build. You can read how we achieved this ambitious goal in the recent blog post of our teammate. Thanks to the knowledge we have gathered using Open Community Build, we have managed to ensure that over 98% of projects passing compilation with 3.1.3 are still compiling correctly without any changes to the source code.

In most cases, fixing an error that resulted from source-incompatible change does not require many fundamental steps for the project author. Typically, it requires explicitly specifying the type in a place where the code previously relied on type inference. If somehow you encounter source incompatibility that cannot be easily fixed, let us know, and we will try to fix that in upcoming patch releases of the 3.2 compiler.

The more important question than “Should I update to Scala 3.2.0?” is “When should I update to Scala 3.2.0?”. The answer for standalone projects and applications is “The sooner, the better”. The update most likely won’t require any additional work, and you will be able to enjoy all the improvements brought by the current version.

If you are a library author, you also need to consider that bumping the compiler version in your library will also force all downstream users to use the new compiler the next time they bump the version of your library. For this reason, we encourage you that if you are following the semantic versioning, you bump the compiler version in the next minor release of your library.

You can read more about our compatibility guarantees and future plans regarding this aspect in the previous blog post.

Known incompatibilities

  • The code that used () to signal that the class constructor should use given instances from the scope as using parameters is no longer compiling.

    class Bar(using x: Int)(y: String)
    given Int = ???
    def test = new Bar()("") // error
    

    You should use Bar("") instead.

  • Nonlocal returns from nested anonymous functions now produce a warning. They will be hard errors at some point in the future.
  • Refutable pattern bindings now produce a warning.

    val option: Option[String] = ???
    val Some(result) = option // Will emit warning
    

    To suppress the warning, you can annotate the right side with the @unchecked annotation.

  • Mirrors for top-level union types are no longer generated.

    Up to 3.1.3, the compiler tried to generate Mirrors for some union types. Even when it succeeded, the result was often incorrect. Now the Mirrors are never generated for any union types.

Contributors

Thank you to all the contributors who made the release of 3.2.0 possible 🎉

According to git shortlog -sn --no-merges 3.1.3..3.2.0, they are:

   154 odersky
    62 Nicolas Stucki
    51 Filip Zybała
    42 Jamie Thompson
    39 Dale Wijnand
    30 Tom Grigg
    29 Paweł Marks
    29 rochala
    25 Guillaume Raffin
    13 Fengyun Liu
    13 Ondrej Lhotak
    11 Guillaume Martres
    11 Sébastien Doeraene
     7 Matt Bovel
     7 Som Snytt
     6 Szymon Rodziewicz
     5 Julien Richard-Foy
     5 Martin Odersky
     5 Wojciech Mazur
     4 Chris Kipp
     4 Olivier Blanvillain
     4 Xavientois
     3 Rikito Taniguchi
     3 Vadim Chelyshov
     2 Adrien Piquerez
     2 Albert Chen
     2 Anatolii Kmetiuk
     2 Arnout Engelen
     2 Jan Chyb
     2 Lan, Jian
     2 Phil
     2 Philippus
     2 Raphael Jolly
     2 Robert Stoll
     2 Tim Spence
     2 cgccuser
     2 gagandeepkalra
     2 manojo
     2 noti0na1
     2 Артём
     1 Aleksander Boruch-Gruszecki
     1 Dmitrii Naumenko
     1 Humberto Rodríguez A
     1 Johannes Rudolph
     1 Jędrzej Rochala
     1 Krzysztof Romanowski
     1 Magnolia.K
     1 Michał Pałka
     1 Ondřej Lhoták
     1 Stéphane Micheloud
     1 Timothée Loyck Andres
     1 Yichen Xu
     1 adampauls
     1 johannes karoff
     1 ouertani
     1 xuwei-k
     1 yoshinorin