Scala : Sandwich de versions et rétrocompatibilité

Introduction

Dans son article https://univalence.io/blog/articles/scala-3-un-nouveau-langage/ François nous présentait les nouveautés de Scala 3 et nous donnait des conseils vis-à-vis de la migration de projets en Scala 2 vers Scala 3.

Dans cet article, nous parlerons un peu plus en détail des problèmes liés aux migrations et cohabitations entre Scala 2 et Scala 3.

Prérequis

Avant de commencer, on commence par les mauvaises nouvelles.

Vous allez avoir des problèmes pour migrer votre code base si :

  • Vous dépendez d’une bibliothèque logicielle qui fait usage de macro Scala 2
    • Si c’est le cas, vous pouvez aller voir ici l’état de la migration des dépendances populaires.
  • Votre projet dépend d’un plugin sbt qui n’a pas d’équivalent pour Scala 3
  • Votre projet dépend de scala reflect
    • Si c’est le cas… je n’ai pas encore de solution pour vous

Jar Hell

Toujours avant de commencer, il est important d’avoir une vue rapide sur ce qu’on appelle le Jar Hell

le Jar Hell c’est ce qui qualifie les problèmes qu’on peut rencontrer avec les mécanismes de classloading de la JVM.

Voilà une liste non exhaustive de ces problèmes :

  • unexpressed deps
    • un jar ne peut pas dire de quels autres jars, il dépend, dès lors c’est au développeur de résoudre lui-même certaines dépendances
  • dépendances transitives
    • Certaines bibliothèques logicielles dépendent de certaines autres bibliothèques (etc.)
  • shadowing
    • Un projet peut dépendre de deux versions différentes d’une même bibliothèque
  • conflit de versions
    • Deux bibliothèques dépendent de deux bibliothèques incompatibles

TASTy ?

Si vous êtes interessé par Scala 3, vous avez surement déjà dû entendre parler de TASTy.

TASTy (Typed Abstract Syntax Trees) c’est un format d’échange entre votre code et votre code compilé (.class file)

Concrètement, ce sont des fichiers qui contiennent toute l’information utile sur le code.

Scala possède un tel outil dans la mesure où les .class contiennent une représentation incomplète du code.

Par exemple, les types génériques subissent un effacement de type

val xs: List[Int] = List(1, 2, 3)

public scala.collection.immutable.List<java.lang.Object> xs();

Cela assure une compatibilité du code source et du code binaire entre les versions de la JVM

Ça vous évite entre autre de recompiler vos applications lorsque vous migrez vers des versions plus récentes de la JVM.

Les avantages d’avoir un format tel que tasty sont multiples :

  • Un outil de build peut s’en servir pour construire le projet pour plusieurs versions de la JVM
  • Permet une inspection du code profonde par des outils de métaprogrammation et permet de générer du code dans des cas avancés

Qu’est-ce que cela autorise à faire de base ?

<<Scala 3 project — uses Scala 2 lib >>

  • la lib ne doit pas dépendre de scala.reflect (donc pas de macro)

<<Scala 2 project — uses Scala 3 lib >>

  • la lib ne doit pas dépendre du nouveau type de métaprogrammation (inline statements…)
  • doit inclure l’option de compilateur -Ytasty-reader

Notez que si vous souhaitez utiliser une bibliothèque Scala 3 dans votre projet Scala 2, vous allez soit :

  • Devoir écrire manuellement que votre lib est en Scala 3
    par exemple :
    "org.legogroup" %% "woof-core" % "0.2.0"
    Deviendra dans un build en Scala 2
    "org.legogroup" % "woof-core_3" % "0.2.0"
    lazy val main = project.in(file("main"))
      .settings(
    		scalaVersion:= scala2,
    		scalacOptions+= "-Ytasty-reader",
    		libraryDependencies++=Seq(
          "org.legogroup" % "woof-core_3"   % "0.2.0"
         )
      )
      .dependsOn(middle)
  • Soit utiliser l’opérateur cross
    lazy val main = project.in(file("main"))
      .settings(
    		scalaVersion:= scala2,
    		scalacOptions+= "-Ytasty-reader",
    		libraryDependencies++=Seq(
          "org.legogroup" %% "woof-core" % "0.2.0" cross CrossVersion.for2_13Use3
         )
      )
      .dependsOn(middle)
    
    sbt Reference Manual — Cross-building

Étude de cas

Nous allons étudier la compatibilité de Magnolia au sein de projets composants Scala 2 et Scala 3

Rappelons que les versions Scala 2 et 3 de Magnolia tirent parti respectivement des macros et des nouvelles fonctionnalités de métaprogrammation.

Nous allons tenter de répondre aux questions suivantes :

  • Peut-on utiliser Magnolia 2 au sein d’un projet Scala 3 ?
  • Peut-on utiliser Magnolia 3 au sein d’un projet Scala 2 ?
  • Est-ce qu’un projet Scala 2 peut dépendre d’un projet Scala 3 qui utilise Magnolia 3 ?
  • Est-ce qu’un projet Scala 3 peut dépendre d’un projet Scala 2 qui utilise Magnolia 2 ?
  • Est-ce que les ADT et Typeclass de Scala 3 peuvent être dérivés par Magnolia 2

Note sur IntelliJ

L’outillage de IntelliJ vis-à-vis de Scala 3 étant encore en développement, nous vous conseillons d’utiliser les commandes sbt build, sbt run depuis un terminal directement

Credit Jonathan WINANDY

todo légende

note ça marche pas :

💩 cette topologie ne marche pas

todo sauf si

todo répondre

  • Peut-on utiliser Magnolia 2 au sein d’un projet Scala 3 ?
    • non mais si on dépend d’un projet s2 on dépede du bon compilo donc macro ok
  • Peut-on utiliser Magnolia 3 au sein d’un projet Scala 2 ?
    • same
  • Est-ce qu’un projet Scala 2 peut dépendre d’un projet Scala 3 qui utilise Magnolia 3 ?
    • oui car on dépend du bon compilateur
  • Est-ce qu’un projet Scala 3 peut dépendre d’un projet Scala 2 qui utilise Magnolia 2 ?
    • idem
  • Est-ce que les ADT et Typeclass de Scala 3 peuvent être dérivés par Magnolia 2
    • enum3 marchent pas mais sinon ok

https://github.com/univalence/scala-sandwich

État de l’art des outils pour aider à la migration

todo sbt migration plugin

todo mima

Conseils

Un jour viendra où il faudra impérativement arrêter d’utiliser des libs en Scala 2 qui ne seront plus maintenues notamment pour des raisons de sécurité. En effet, être compatible avec Scala 3 ne signifie pas que vous n’aurez jamais à migrer.

Voici quelques conseils pour préparer l’échéance :

  • Effectuer la migration petit a petit pour fixer les problèmes petit à petit
  • Arrêter le support des applications en Scala 2.12 et antérieures pour faciliter le passage a Scala 3
  • Faites des tests de non-régression avec une bonne couverture de test (80% semble être une bonne recommandation)
  • Lisez la doc de Scala à propos de la migration
  • Regardez l’état de la migration des libs dont vous dépendez
  • Arrêtez d’utiliser ce que vous savez qui ne passera pas en Scala 3 (les macros, scala reflect)

Erreurs

signature has wrong version

[error] (class scala.tools.tasty.UnpickleException/TASTy signature has wrong version.
[error]  expected: {majorVersion: 28, minorVersion: 1}
[error]  found   : {majorVersion: 28, minorVersion: 2}

Cela arrive quand les versions de scala 2.13 et scala 3 ne sont pas compatible, il faut en général monter la version de scala 2.13 (la dernière c’est 2.13.9)

inline in scala 2

[error] scala.reflect.internal.Types$TypeError: Unsupported Scala 3 inline method deriveSubtype; found in trait magnolia1.Derivation.


[error] stack trace is suppressed; run last c / Compile / compileIncremental for the full output
[error] (c / Compile / compileIncremental) scala.reflect.internal.Types$TypeError: Unsupported Scala 3 inline method deriveSubtype; found in trait magnolia1.Derivation.

WeakTypeTag evidences

[error][...] scala:26:51: macro implementations cannot have implicit parameters other than WeakTypeTag evidences
[error]implicit def gen[T]: Show[T] = macro Magnolia.gen[T]

Arrive si scala reflect est manquant dans un projet magnolia - Scala 2


  • old

Comment je fais pour créer un projet en Scala 3 qui dépend d’un module en Scala 2

Bonne nouvelle, c’est faisable et vous devriez y arriver facilement avec l’option dependsOn de sbt.

ThisBuild / version := "0.1.5"
ThisBuild / scalaVersion := "3.1.0"

lazy val scala3project = project.in(file("scala3project"))
  .settings(scalaVersion := "3.1.0")
  .dependsOn(core)

lazy val core = project.in(file("core"))
  .settings(
    scalaVersion := "2.13.8",
    libraryDependencies ++= Seq(
      "foo.org" %% "myScala2Lib"% "1.1.2",
    )
  )

Notez que vous risquez d'ici à quelques années de vous retrouver dans ce genre de situation :

En effet, les bibliothèques logicielles vont peu à peu arrêter le support de leurs lib en Scala 2

Mais c’est possible aussi.

Notez aussi que si votre projet dépend d’une version de Scala supérieur à Scala 2.13.4, vous bénéficiez de l’option de compilation -Ytasty-reader qui vous permet d’avoir une compatibilité ascendante entre Scala 2 et Scala 3

J’ai un projet Scala 2 qui utilise Magnolia dont un module en Scala 3 qui dépend de Magnolia, il y a moyen que ça fonctionne ?

D’accord, là, on commence à chercher la mouise…

D’une part, c'est un ticket pour le Jar Hell (shadowing, dépendances transitives). Le projet A en Scala 2 dépend 2 fois de magnolia dans des versions pour des versions de Scala différentes.

Magnolia pour Scala 2 fait usage des macros. Le code généré utilise Magnolia pour Scala2, qui va se faire shadow au runtime par Magniola pour Scala 3 (c’est incompatible)

✅ Cette topologie fonctionne si :

  • On garde qu’une seule version de Magniola
  • Et l'on dérive les typeclasses avec le bon compilateur :
    • Dans le Scala 3 module si on utilise magniola pour Scala 3
    • Dans le Scala 2 project si on utilise magniola pour Scala 2

_

  • ce qui ne marche pas : Deriver avec du magniola 2, dans un compilateur scala 2 des formes qui viennent de Scala 3


Mais on peut se demander :

  • Si les macros s’étendent au compiletime, pourquoi on ne pourrait pas build le projet Scala 3 module avec magnolia 3 et faire usage du tasty-reader par la suite si tant est qu’on rende la dépendance à magnolia intransitive ?

D’une part, pour votre santé mentale, j’espère que vous ne vous poserez pas cette question régulièrement… Allez voir des amis, faites des trucs sains svp

On a été sympa, on a fait le test pour vous et effectivement ça ne fonctionne pas.

Vous pouvez en revanche utiliser la version de Magnolia pour Scala 3 dans votre projet en Scala 2 sans trop de soucis.

Est-ce que ça, ça marche ?

todo test mais a priori plus de jar hell, a voir si le cas scala 2 est possible

J’ai un projet Play en Scala 2 je veux ajouter un contrôleur en Scala 3, comment fais-je ?

On peut imaginer vouloir anticiper la migration de son application Play petit à petit en commençant à écrire les nouveaux contrôleurs en Scala 3 par exemple.

Il y a plusieurs raisons de ne pas faire ça :

  • D’une part Play est basé sur Akka et Akka http qui vient semble-t-il de passer en closed source… Donc peut-être qu’il est temps de réévaluer vos choix technologiques si vous ne voulez pas subir un changement d’état au niveau de votre porte-monnaie…
  • Aussi, Play est en cours de migration vers Scala 3 et le projet en est qu’au début. C’est probablement encore un peu tôt pour avoir ce genre de considérations.

todo test