Continuant progressivement son cheminement vers la version 3 et après ~1500 pull requests, la version 2.13.0 de Scala est sortie il y a quelques jours. Les nouveautés apportées par cette version concerne un refactoring de l'API collection, quelques modifications dans le SDK, de nouvelles possibilités dans le langage et des améliorations du côté du compilateur. Je vous propose de passer en revue les modifications qui m'ont le plus intéressées.

Collection

Le plus gros du travail de Scala 2.13.0 est avant tout le refactoring de l'API collection. L'un des objectifs de cette nouvelle version est une simplification du code : une réduction dans la hiérarchie des types, la conservation des méthodes réellement nécessaires, la création d'un package dédié aux collections parallèles en plus des package mutable et immutable, la disparition de CanBuildFrom... Ces simplifications apportent une meilleure lisibilité du code de l'API et permettent d'obtenir de meilleures performances. Elles ont aussi comme particularité d'apporter plus de cohérence.

Sinon, LazyList est proposé en alternative à Stream. La différence est que cette nouvelle collection conserve en mémoire les résultats déjà calculés. Elle peut donc s'avérer plus performante que Stream.

Côté interopérabilité avec Java, les outils dédiés sont placés dans un package dédié : scala.jdk. Ils inclus en plus l'interopérabilité avec les streams de Java, avec le type Optional et les interfaces fonctionnelles prédéfinies du JDK. Il a beaucoup à dire sur cette nouvelle API collection et cela nécessiterait un article à part entière.

Il faut savoir qu'un backport de cette nouvelle API est proposé pour les versions 2.11 et 2.12 de Scala avec le projet scala-collection-compat. Ce backport inclut des règles de migration Scalafix pour :

D'une manière ou d'une autre, la volonté de ceux qui font Scala et son SDK est de vous pousser à abandonner l'ancienne API quelque soit la version de Scala adoptée, afin de migrer vers une API qui paraît plus stable, plus accessible et mieux conçue. Il n'y a néanmoins aucune garantie que la migration se fera sans heurt. Si l'outillage assure une migration minimisant les problèmes à la compilation, des changements en terme de comportement sont probablement à prévoir.

Autres nouveautés

L'utilisation des string interpolator dans le pattern matching, dans le cadre d'une affectation.

val date = "2000-01-01"
val s"$year-$month-$day" = date
// year = 2000 - month = 01 - day = 01

Et avec une structure match...case.

def dateComponentOf(date: String): Option[(Int, Int, Int)] =
  date match {
    case s"$year-$month-$day" => Option((year.toInt, month.toInt, day.toInt))
    case s"$day/$month/$year" => Option((year.toInt, month.toInt, day.toInt))
    case _ => None
  }

Cette fonctionnalité permet de réaliser des analyses de chaîne de caractères simples. Elle est moins puissante que les regexp, mais plus accessible.

Scala propose maintenant dans sont SDK une alternative à la fonctionnalité try-with-resources de Java. scala.util.Using tient ce rôle. Il est applicable à des ressources de type Releasable, ou plus exactement de catégorie Releasable[R], car il s'agit d'une typeclasse. Cerise sur le gâteau, une instance Releasable est applicable à tous les types dérivant de AutoCloseable, par conversion implicite. Ce qui permet d'utiliser Using avec une bonne partie des types ressources du monde Java ! Une différence majeure avec la version Java est le fait que Using est une expression. Comme elle retourne une instance de type Try, on peut l'utiliser dans un for-comprehension. Néanmoins, comprenez que Using est en évaluation stricte (son contenu est exécuté directement lors de son évaluation dans le code). Pour une version non-stricte, tournez-vous vers la fonctionnalité bracket de Monix et de ZIO.

val content1: Try[String] =
  for {
    _ <- Using(new BufferedWriter(new FileWriter(file))) {
      _.write("Hello world")
    }
    c <- Using(new BufferedReader(new FileReader(file))) {
      _.readLine()
    }
  } yield c
println(s"file content 1: $content1") // print file content 1: Success(Hello world)

val content2: Try[String] =
  for {
    _ <- Using(new BufferedWriter(new FileWriter(file))) {
      _.write("Hello world")
    }
    c <- Using(new BufferedReader(new FileReader(file))) { f =>
      f.close() // in a view to have an exception ;)
      f.readLine()
    }
  } yield c
println(s"file content 2: $content2") // file content 2: Failure(java.io.IOException: Stream closed)

L'opération tap peut être ajouté à toute sorte de valeur dans une expression. La fonction retourne la valeur elle-même et s'apparente en ce sens à la fonction identity. Sauf que tap va vous permettre d'introduire des effets de bord dans vos expressions. Son utilisation doit bien entendu être limité au débogage là où il était difficile à introduire auparavan

import scala.util.chaining._

val result1 = "hello".tap(println) + " world" // print hello
val result2 = "hello" + " world"

println(result1 == result2) // print true

Scala 2.13 accepte le caractère "_" comme séparateur numérique. Les valeurs suivantes sont identiques :