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 :

  • soit réécrire le code pour une version compatible avec la 2.13 ou plus, si vous n'avez pas besoin de cross-compilation avec des versions précédentes (ie. compilation sur plusieurs versions de Scala),
  • soit réécrire le code en conservant une compatibilité avec la version 2.12 et en introduisant une dépendance vers scala-collection-compat.

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 auparavant

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 :

100000
100_000
10_00_00

Cette fonctionnalité existe déjà dans Java. Elle permet de faciliter la lecture du code en particulier lorsqu'on doit hard coder des grands nombres.

L'unification partielle est maintenant tout le temps active et l'option -Ypartial-unification est retirée. Cette fonctionnalité permet d'utiliser la forme F[_] dans la déclaration des type — forme très appréciée par les développeur Scala "type level". Vous trouverez plus de détails dans cette section parlant de SI-2712 dans mon article sur Scala 2.12.

Les flèches au format unicode sont dépréciées et c'est une très bonne chose. Une flèche comme pose problème car elle n'a pas la même priorité que sont équivalent ASCII ->. Elles sont donc pas interchangeables. Ainsi, l'expression 1 -> 2 / 4.0 donne (1, 0.5), alors que 1 → 2 / 4.0 donne une erreur de compilation : value / is not a member of (Int, Int). Il faut ajouter des parenthèses pour obtenir le même résultat 1 → (2 / 4.0). La dépréciation de cette fonctionnalité devrait éviter pas mal de confusions.

L'implicite any2stringadd définit dans Predef et responsable de comportements "inappropriés" dans Scala est déprécié. La syntaxe procédurale (def m() {}) aussi. Il faudra désormais utiliser une signature complète (def m(): Unit = {}). Les litéraux de type symbole ('symb) sont dépréciés pour cette notation Symbol("symb").

Du côté du compilateur

Côtés compilateurs, les optimisations promettent une amélioration des performances de 5 à 10 % à la compilation. Et côté runtime, la nouvelles API collections apporte de meilleurs performances en donnant notamment au compilateur la possibilité de mettre en place plus d'inlining (le fait de replacer des appels de fonctions par leur contenu). Petite aide appréciable : le compilateur vous fera des suggestions en cas de faute de frappe sur des noms d'identifiants.

Au niveau option de compilation, deux nouveaux types d'options seront créés, allégeant ainsi -X et -Y :

  • -V pour toutes les options permettant d'afficher des informations sur le fonctionnement du compilateur (eg. -Vphases affichage des phases de compilation, -Vdoc affichage des activités de scaladoc).
  • -W pour toutes les options des gestions des avertissements à la compilation (eg. -Wdead-code avertissement en cas de code inutilisé, -Wunused:imports avertissement en cas d'imports inutilisés).
  • À ce propos, -Xfatal-warnings va devenir -Werror. Les deux options existent en 2.13. Mais la release note indique que -Werror est recommandée. -deprecation devient -Xlint:deprecation et -Xfuture devient -Xsource:2.14.

Car oui, il y aura une version 2.14 qui devrait sortir mi-2020, peu avant la version 3.

Côté SBT, Scala 2.13 nécessite d'utiliser au minimum SBT 1.2.8 ou 0.13.18. Sinon, ça fonctionne bien avec Maven et Gradle.

Conclusion

Cette nouvelle version de Scala 2 laisse entrevoir un premier lot de modifications dans le langage vers à la fois plus de maturité et de pragmatisme dans la conception du SDK et les choix appliqués dans les fonctionnalités du langage et le compilateur. Mais elle laisse aussi entrevoir plus de fun en rendant le langage et ses fonctionnalités plus accessibles. Il reste à voir comment se passera la migration vers Scala 3 proposé par ceux qui font le langage.

Actuellement, mon regard se tourne vers Spark, sachant que ce framework est aussi connu pour ses temps de latence assez long pour les montées de versions vis-à-vis de Scala et que les mainteneurs de lib abandonnent progressivement Scala 2.11. Néanmoins, un travail est en cours pour intégrer la 2.13.

Le code présenté ici est disponible sur Github.

Photographie par Lindsay  Henwood sur Unsplash.