Le compilateur Scala

Scala est un langage qui propose un style de programmation à la fois fonctionnel et orienté objet. Il est connu à travers Spark, le framework populaire dans le domaine du BigData. Mais aussi par l'intermédiaire d'autres frameworks comme Play, Akka ou Gatling. Ce langage est relativement proche de Java, tout en offrant des constructions facilitant la concision du code et l'adoption d'un style de programmation fonctionnel. Quant à son compilateur, il offre un véritable rempart dans le but de limiter autant que possible l'émission de bugs en production. Mais, alors que Scala 3 a été annoncé récemment, connaissez-vous vraiment le compilateur Scala ?

Au-delà de la production de bytecode pour la JVM, il se trouve que le compilateur n'est tout d'abord pas unique et qu'il existe des versions du compilateur Scala permettant aussi de produire du code compilé pour d'autres plateformes. De plus, il existe plusieurs options, dont le but est d'obtenir plus de garanties en termes de qualité de code. C'est ce que nous allons voir dans cet article.

Les compilateurs Scala

Il n'y a pas qu'un seul compilateur Scala ! Il y en a trois reconnus, plus quelques compilateurs dérivés.

Scalac et autres projets liés

Scalac est le compilateur officiel, disponible sur le site Web de Scala. Il a été créé en 2003 par Martin Odersky à l'EPFL. Il est en version 2 depuis 2006. Initialement, Scalac est prévu pour convertir les codes écrits en Scala en bytecode pour plateforme JVM. Si le compilateur est la propriété de l'EPFL, il est développé sous licence BSD-3 (et bientôt APL-2). C'est donc un projet Open Source. Il est hébergé sur GitHub et tout le monde peut y contribuer. La communauté fournit une documentation complète, ainsi qu’un salon Gitter et un site Discourse pour que vous puissiez participer au développement.

Assez rapidement après la naissance de Scala, un projet nommé Scala.NET a été lancé pour que Scala fonctionne sur plateforme CLR (ou Mono/.NET) en se basant sur IKVM, qui est une implémentation Java pour Mono et .NET. Mais le projet Scala CLR n'a pas eu de suite.

En 2013, le projet Scala.js est lancé. Il s'agit d'une adaptation du compilateur Scala permettant de convertir le code écrit en Scala en JavaScript. Une grande partie des libs populaires Scala sont adaptées pour Scala.js et des libs JavaScript populaires ont une façade prévue pour ce compilateur. Scala.js est disponible en version 0.6.23 et la version 1.0 est en cours de préparation. Ce compilateur est actuellement utilisé en production.

En 2017, le projet Scala Native est lancé. Il s'agit dans ce cas d'une adaptation du compilateur Scala permettant de convertir le code écrit en Scala en code natif en se basant sur LLVM.

Dotty

Dotty est un compilateur expérimental et représente la direction prise par les futurs versions de Scala à des fins d'amélioration diverses. Ce projet a démarré en 2013. Il est actuellement prévu que Dotty soit in fine Scala 3 (slides).

Dans son état actuel, Dotty ajoute à Scala toute une série de fonctionnalités impactant le langage, dont certaines cherchent à résoudre les problèmes énoncés par la communauté :

  • Un compilateur plus performant ;
  • Une structure pour déclarer des types énumérés, voire des ADT (Algebraic Data Type) ;
  • Des idiomes plus simples pour la programmation au niveau type ;
  • La fin de la limitation à 22 éléments pour les tuples et les paramètres de fonctions ;
  • Plus de sécurité vis-à-vis de la valeur null ;
  • Moins de bizarreries, comme celles découlant d'une sur-utilisation des implicites ;
  • etc.

Nous y trouvons aussi un format pivot, appelé TASTY, pour la sérialisation de l'arbre syntaxique (ou Abstract Syntax Tree / AST) obtenu dans les premières phases de la compilation. TASTY doit permettre de mieux unifier les différents compilateurs Scalac, Scala.js et Scala Native. Il doit aussi permettre de faciliter la mise en place de macros (méta-programmation à la compilation), offrir une meilleure compatibilité avec l'écosystème Java, améliorer l'intégration dans les IDE et permettre la création d'outils d'analyse et d'intervention sur le code plus performants, comme les outils de vérification de style, de linting ou de recherche automatique de bugs.

L'intégration des fonctionnalités de Dotty dans Scala se fait au fur et à mesure des versions de Scalac. Pour le moment, une version 2.13 devrait sortir dans l'année, puis une version 2.14 un peu plus tard, avant la sortie de Scala 3 au début de 2020. Ainsi, la migration vers Scala 3 devrait se faire en douceur.

À noter que Dotty est stable, mais les fonctionnalités apparaissant dans ce compilateur ne sont pas encore arrêtées. N’utilisez Dotty que si vous voulez avoir un avant-goût de ce que pourra être Scala 3. Et pour cela, vous pouvez passer par l'éditeur de Scala en ligne Scastie avec cet exemple !

Typelevel Scala

Typelevel Scala est un fork réalisé par la communauté Typelevel du compilateur Scala officiel.

Ce fork est intervenu en 2014, à un moment où Scala était perçu comme une possession de la société Lightbend (appelée auparavant Typesafe). Le reproche fait à Lightbend était d'emmener Scala vers un style de programmation plus adapté aux développeurs Java, représentant le langage adopté par la plupart des clients de Lightbend pour leurs projets. Selon la communauté Typelevel, cette orientation s'oppose à l'adoption d'un style de programmation permettant de mettre en place les dernières avancées en termes de programmation fonctionnelle. Typelevel, hébergeant plusieurs projets dans cet objectif, voyait alors en cette prise de direction une menace et justifiait ainsi le fork.

Une guerre de chapelles aurait pu naître de cette scission. Mais finalement, la communauté Scala a bien réagi. Actuellement, cette communauté est régie en sous-communautés (dont Typelevel fait partie) avec des appétences différentes (programmation fonctionnelle, big data, développement Web...). Chaque sous-communauté a la possibilité d'apporter un regard critique sur les évolutions de Scala et d'y contribuer.

Ainsi, l'objectif du projet de compilateur Scala de Typelevel est d'apporter au compilateur officiel des améliorations et des corrections permettant un style de programmation d'autant plus fonctionnel, voire orienté type. La compatibilité avec Scala est toutefois conservée et des améliorations de Typelevel Scala sont régulièrement remontées dans Scalac.

Préférez toutefois utiliser la version officielle de Scalac, sauf si vous êtes aguerris aux concepts de la programmation fonctionnelle et que le compilateur officiel ne vous suffit pas. Ce compilateur peut être testé avec Scastie (voir la partie Build Settings).

L'outillage disponible avec le compilateur

Le compilateur Scala possède de nombreuses options. La plupart de ces options permettent d'optimiser le code, d'en altérer le comportement, d'ajouter des plugins de compilation ou d'obtenir des informations et des statistiques sur la compilation. Scalac (ainsi que ses forks) possède aussi de nombreuses options permettant de remonter selon le cas des alertes ou des erreurs si le style du code ne convient pas. Nous allons explorer une partie de cette catégorie d'options dans cette section.

Les options de compilations disponibles varient d'une version de Scala à une autre. Un simple scalac -help est un point d'entrée pour en savoir plus. Néanmoins, la documentation de ces options reste succincte et peut parfois sembler abstraite, à défaut d'exemples permettant de comprendre leurs implications. Il reste dans ce cas à faire un tour pour les plus courageux d'entre-vous dans le code source du compilateur (cf. GitHub [1], [2]).

Au niveau des options standards et avancées (pour la liste complète : scalac -help ou scalac -X), voici quelques exemples :

  • -Xfatal-warnings : transforme tout warning en erreur fatale. Cette option est intéressante au niveau de la CI ou dès lors qu'un développeur souhaite partager ses modifications. Elle permet de ne livrer un code qu'avec un niveau de qualité pré-déterminé ;
  • -language:... : active ou désactive des fonctionnalités du langage, comme la notation postfixée (1 to 10 toList, sans utiliser de point) ou la possibilité de définir des macros (au delà de leur utilisation). Cette option est à utiliser si vous êtes assez à l'aise avec Scala ;
  • -deprecation : comme Java, émet un warning dès lors que du code est marqué comme deprecated. N'hésitez pas à l'utiliser ;
  • -explaintypes : donne plus de détail sur les erreurs de typage.

Au niveau des options avancées d'alerte spécifique (scalac -Xlint:help pour la liste complète), voici quelques exemples :

  • -Xlint:infer-any : une alerte est émise lorsqu'un paramètre de type (de la forme MonType[A]) est par induction positionné à Any. Par exemple, pour scalac, List(1.0, "hello") aura pour type List[Any]. Le paramètre lèvera une alerte dans ce cas. Par contre, si vous indiquez List[Any](1.0, "hello"), il n'y aura pas d'alerte ;
  • -Xlint:doc-detached : émet une alerte lorsque la Scaladoc est détachée de l'élément documenté (du fait de la présence de lignes vides, par exemple) ;
  • -Xlint:private-shadow : émet une alerte lorsque qu'un champ privé cache un champ défini dans la classe parente, par exemple parce qu'ils possèdent le même nom. Cette situation peut en effet être une source de confusion.
  • -Xlint:nullary-unit : émet une alerte si une fonction nullary retourne Unit. Une fonction nullary est une fonction qui est déclarée sous la forme def myFunction: Type = ... (pas de parenthèse lors de la déclaration de la fonction). Du point de vue de la programmation fonctionnelle, si une fonction ne retourne pas de valeur, elle n'est donc pas utile sachant qu'elle n'est pas censée avoir d'effet de bord. Si toutefois, un effet de bord est souhaité (eg. changer l'horloge système), il peut être intéressant d'avoir une telle fonction. Néanmoins, Scala dans son SDK possède des fonctions (avec paramètres) qui retournent Unit, mais pas de fonction nullary de ce type. Si vous souhaitez adopter un style fonctionnel, cette option va vous y aider.

Et pour les options privées (scalac -Y pour la liste complète), voici quelques exemples :

  • -Ywarn-dead-code : émet une alerte lorsque du code ne peut pas être exploité. Laisser du code mort s'avère néfaste à la lisibilité du code ;
  • -Ywarn-unused-import : émet une alerte lorsque des imports de classe ne sont pas nécessaires ;
  • -Ywarn-unused:... : version étendue de l'option précédente pour appliquer la vérification aux membres privés (privates), aux variables locales (locals), aux paramètres (params), etc. Il n'est pas forcément nécessaire d'activer toutes ces options. On peut par exemple autoriser le fait d'avoir des paramètres de fonction inutilisés pour des questions d'homogénéité d'interface ;
  • -Ywarn-value-discard : émet une alerte si le résultat d'une expression qui n'est pas de type Unit n'est pas utilisé. En effet, si au niveau d'une ligne de code vous effectuez un calcul et que vous n'en faites rien (pas d'affectation de variable, pas de valeur retournée par une fonction...), votre ligne de code est a priori inutile du point de vue de la programmation fonctionnelle. Par contre, cette ligne de code peut déclencher un effet de bord (par exemple, la modification de l'heure du système). S'il s'agit d'un comportement attendu, cette option sera inutile. D'un autre côté, activer cette option vous poussera à adopter un style de programmation fonctionnel.
  • -Ywarn-numeric-widen : émet une alerte si une valeur numérique risque d'être convertie en un type numérique "plus large". L'alerte est déclenchée par exemple lorsque vous utilisez une fonction f qui prend en paramètre un Long et que vous lui passez un Int. Pour éviter l'alerte dans ce cas, il faudra utiliser un cast explicite vers Long de la valeur fournie : val result = f(myInt: Long).

Les options du compilateur Scala s'ajoutent assez simplement dans SBT, de la manière suivante :

scalacOptions ++= Seq(
  "-deprecation",
  "-Xfatal-warnings",
  // ...
)

Et au niveau de Maven, de la manière suivante

<plugin>
    <groupId>net.alchim31.maven</groupId>
    <artifactId>scala-maven-plugin</artifactId>
    <version>3.3.2</version>
    <configuration>
        <args>
          <arg>-deprecation</arg>
          <arg>-Xfatal-warnings</arg>
          <!-- ... -->
        </args>
    </configuration>
</plugin>

Afin de bénéficier d'exemples plus concrets et de ce qu'il convient de mettre en place, n'hésitez pas à aller fouiller dans les repos Github de projet en Scala, comme SBT ou Doobie.

Conclusion

Comme nous avons pu le voir, le compilateur Scala (ou plus exactement Scalac) est en évolution constante. La façon dont la communauté est gérée aide Scala à évoluer sur différents axes et aussi sur différentes plateformes. Nous avons également différentes versions du compilateur Scala, mais avec des objectifs différents. Chaque compilateur représente ainsi une voie d'expérimentation et d'exploration quasiment unique. Et tout ceci est accessible à tous.

Dans son utilisation, le compilateur Scala (ainsi que les autres compilateurs de son écosystème) dispose d'un outillage assez important à travers ses options pour imposer des contraintes sur la qualité du code. Nous en avons vu quelques unes ici. Par rapport à ce que nous trouvons dans l'écosystème Java, l'outillage fourni par le compilateur Scala peut paraître rudimentaire. Mais nous ne sommes restés qu'au niveau de ce que propose le compilateur seul. Il existe bien d'autres outils dans l'écosystème Scala qui vous assureront un confort suffisant dans la création de vos applications. Faites pour cela un tour des projets du Scala Center.