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, débutons 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- Si c’est le cas, allez jeter un œil aux alternatives.
- 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 que l'on appelle le Jar Hell. Le Jar Hell c’est ce qui qualifie les problèmes que l'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 n'indique pas 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 (qui dépendent elles-mêmes d’autres bibliothèques, etc.). C’est, là encore, au développeur de récupérer les sous-dépendances.
- Shadowing
- Un projet peut dépendre de deux versions différentes d’une même bibliothèque. Cela arrive, par exemple, lorsque deux des dépendances d’un projet dépendent d’une même bibliothèque, mais pour des versions différentes. Dans le meilleur des cas, il est possible de ne conserver que la version la plus récente. Mais il peut arriver que les deux versions soient incompatibles (par exemple, une fonction est disponible dans les deux versions, mais la signature a changé avec des paramètres différents et/ou un type de retour différent).
- Conflit de versions
- Deux bibliothèques dépendent de deux bibliothèques incompatibles.
TASTy ?
Si vous êtes intéressé par Scala 3, vous avez surement déjà dû entendre parler de TASTy.
TASTy (Typed Abstract Syntax Trees) 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.
Le format TASTy permet de palier à certains désavantages de la compilation en bytecode, les fameux .class
, dans la mesure où ces derniers contiennent une représentation incomplète du code.
Par exemple, une fois compilé en bytecode (.class
), les types génériques subissent un effacement de type (type erasure).
Le code Scala suivant :
val xs: List[Int] = List(1, 2, 3)
sera converti en
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, pour des raisons historiques, puisque les premières versions de Java ne disposaient pas de types génériques (implémentés dans Java 1.5 par Martin Ordersky). Cela vous évite entre autres 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 ?
- La lib ne doit pas dépendre de
scala.reflect
(donc pas demacro
).
- La lib ne doit pas dépendre du nouveau type de métaprogrammation (
inline
statements…). - 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-buildinghttps://www.scala-sbt.org/1.x/docs/Cross-Build.html#Scala+3+specific+cross-versions
É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 pour Scala 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.
Une précision sur le schéma :
genX[TcY[ADTZ]]
signifie que la classe expose une dérivation via magnolia pour Scala X, sur des typeclass écrites en Scala Y paramétrés par des ADT Scala Z avec X, Y et Z pouvant être égal à 2 ou 3.
Notez que la topologie suivante ne marche pas :
✅ Cette topologie fonctionne si :
- On garde qu’une seule version de Magnolia
- Et l'on dérive les typeclasses avec le bon compilateur :
- Dans le
Scala 3 module
si on utilisemagniola pour Scala 3
- Dans le
Scala 2 project
si on utilisemagniola pour Scala 2
- Dans le
Répondons aux questions précédemment posées :
- Peut-on utiliser Magnolia 2 au sein d’un projet Scala 3 ?
- Non. En effet, générer une typeclass via Magnolia 2 fait usage de
scala.reflect.macro
, ce qui est impossible à faire avec le compilateur de Scala 3 (ces fonctionnalités ont été abandonnées).
- Non. En effet, générer une typeclass via Magnolia 2 fait usage de
- Peut-on utiliser Magnolia 3 au sein d’un projet Scala 2 ?
- Non plus, ici ce sont les nouvelles fonctionnalités de métaprogrammation qui ne sont pas supportées par le compilateur de Scala 2.
- 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 ?
- Oui aussi, pour les mêmes raisons que précédemment.
- Est-ce que les ADT et Typeclass de Scala 3 peuvent être dérivés par Magnolia 2
- Les enums de Scala 3 semblent poser un problème lorsqu’on tente de les dériver en Scala 2. En revanche, les ADT et typeclass de Scala 3, même avec leurs nouvelles features, paraissent ne pas poser problème, ni à l’utilisation, ni à la dérivation.
Si vous souhaitez expérimenter un peu avec les sandwiches, vous pouvez démarrer avec le code de cet article : https://github.com/univalence/scala-sandwich
Les outils pour aider à la migration
sbt est livré avec un outil de migration sbt migration plugin
.
Il permet de :
- Migrer les dépendances disponibles en Scala 3.
- Migrer les options de compilation vers leur équivalent en Scala 3.
- Aider à migrer la syntaxe.
- Repérer les types qu’il sera impératif d’annoter manuellement du fait que le mécanisme d’inférence de type change en Scala 3.
Si vous avez des dépendances de versions de Scala différentes, vous pouvez envisager d’utiliser Mima pour prévenir les incompatibilités binaires.
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 progressivement 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 (un couverture autour de 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 les technologies/features dont vous savez à l’avance qu’elles ne seront pas portées 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]
Cela arrive si scala-reflect
est manquant dans un projet magnolia - Scala 2.