Scala 2.12 : Tour d'horizon des nouveautés

La version 2.12 de Scala est sortie dans sa première release au début du mois de novembre. Comme toute version importante de n’importe quel langage ou framework, celle-ci arrive avec son lot de nouveautés, d’améliorations, d’incompatibilités et autres bugs (qui ne manqueront pas d’être fixés dans des correctifs à venir). Par contre, l’une des nouveautés qui a particulièrement attiré mon attention, c’est le travail cherchant à unifier le monde de Scala et celui qui est apparu avec Java 8 : les lambdas expressions et ce qui les a accompagnées.

Je vous propose de découvrir les nouveautés de Scala 2.12 à travers trois articles. Le premier article (c’est-à-dire celui-ci) fait un rapide tour d’horizon de ces nouveautés, dont la correction du bug SI-2712. Dans un deuxième article, nous nous intéresserons aux rapprochements entre Scala et Java sur le plan des interfaces fonctionnelles. Le troisième et dernier article détaillera la transcription des lambda expressions dans Scala.

Un tour d’horizon sur les nouveautés

Scala 2.12 met en place un nouvel optimiseur dans le compilateur, permettant d’alléger d’autant le bytecode produit. Cet optimiseur retire par défaut le code mort (inatteignable). Il est accessible en utilisant l’option -opt. Selon le paramètre fourni à cette option, l’optimiseur peut aussi inliner des méthodes finales avec un scope allant des fichiers du projet à l’ensemble du classpath importé. Il peut inliner les closures définies et appelées dans une même méthode, supprimer le boxing/unboxing de types primitifs et de tuples, etc. Pour plus d’information, tapez la commande scalac -opt:help.

Le nouveau générateur de bytecode GenBCode est désormais utilisé. Ce générateur devrait être plus rapide par rapport aux générateurs utilisés précédemment (15 % en moyenne) et produire du code plus compact.

Dans l’outillage Scala 2.12 est proposée une nouvelle présentation de la ScalaDoc. On vous laisse apprécier… ou pas.

Les conversions implicites entre collections Java et les collections Scala (comme scala.collection.JavaConversions et certains éléments du package scala.collection.convert) sont dépréciées. Il faut explicitement importer les méthodes de JavaConverters et appeler les méthodes .asJava / .asScala au niveau des collections.

Enfin, Scala 2.12 se sépare définitivement de sun.misc.Unsafe. Cette classe mise en place par les développeurs Oracle du JDK, non officielle et mal cachée (beaucoup de frameworks l’utilisent) permet de manipuler des éléments de la JVM à un niveau particulièrement bas (d’où son nom). Il est en général vivement recommandé de ne pas l’utiliser sachant qu’elle va subir un certain nombre de migrations.

Scala utilise SBT pour construire Scala. Pour plus d’information sur SBT, nous vous renvoyons aux articles de Jean-Philippe Bunaz (partie 1, partie 2, partie 3).

Scala 2.12 donne à présent l’accès aux noms des paramètres de méthodes selon le JEP-118. Ce qui rend ces paramètres disponibles en utilisant l’API réflexion de Java.

SI-2712

Le SI-2712 est un très vieux bug qui date de 2009. Ce bug a été souvent décrié par les développeurs “typelevel” (notamment, par @mandubianhotep PSUG après PSUG ;p). Sa présence a nécessité pas mal de contournements dans des libs comme scalaz ou cats. Il a été corrigé par Miles Sabin et mis à disposition dans la version 2.11 en tant que fonctionnalité expérimentale. L’activation de cette fonctionnalité est accessible dans Scala 2.12 en utilisant l’option -Ypartial-unification.

Pour résumer, il est maintenant possible d’unifier un type paramétré de la forme F[_] (F pouvant être une liste, une fonction, un future et F[_] signifiant que F est forcément un type générique) avec des types de la forme Either[I, _] ou I => _ (transcrit en Function1[I, _]). Ce que ne faisait pas le compilateur auparavant, puisqu’il voyait F[_] comme n’ayant qu’un paramètre et Function1[I, _] comme ayant deux paramètres. Daniel Spiewak décrit assez bien dans un gist les tenants et aboutissants de cette correction de bug.

Rentrons dans les détails…

Pour comprendre ce qui se passe dans le cadre du SI-2712, je commence par déclarer une typeclass Functor. La typeclass est une particularité du monde de la programmation fonctionnelle pour mettre en place le polymorphisme ad hoc. Une typeclass est une sorte d’interface (au sens Java), qui offre la possibilité d’étendre dynamiquement des types existants, autrement dit au runtime. En programmation objet classique, cette extension n’est applicable qu’aux classes et n’est possible que statiquement, c’est-à-dire au compile-time, sauf en utilisant la réflexion. Functor est une façon de catégoriser certains types paramétrés (comme List, Option ou Future), qui possèdent une opération que nous appellerons fmap permettant de transformer la valeur interne sans modifier la structure.

Définition de Functor

En Scala, les typeclass sont écrites de la manière suivante :

trait Functor[F[_]] {
  def fmap[A, B](fa: F[A])(f: A => B): F[B]
}

Ce que disent ces lignes, c’est que Functor est un type qui peut s’appliquer à des types génériques F de la forme F[_] (ie. avec un seul paramètre, comme les listes, les options, les futures, etc.). Un Functor contient la fonction fmap, dont le premier paramètre est une instance de F et dont le second paramètre permet de convertir le contenu de F.

Par exemple, le code ci-dessous permet d’utiliser la méthode fmap sur des listes.

implicit val listFunctor =
  new Functor[List] {
    override def fmap[A, B](fa: List[A])(f: A => B): List[B] =
      fa.map(f)
  }

Dans ce code, le implicit est utile pour le code ci-dessous.

implicit class FunctorOps[F[_], A](fa: F[A])(implicit val functor: Functor[F]) {
  def fmap[B](f: A => B): F[B] =
    functor.fmap(fa)(f)
}

La classe ci-dessus permet d’ajouter automatiquement la méthode fmap à toute instance de la catégorie Functor. Pour cela, on vérifie au niveau du premier paramètre de FunctorOps que le type de l’instance à la forme F[A] (ce qui fonctionne avec les listes, mais pas avec les entiers). On recherche ensuite dans le scope une déclaration implicite de la forme Functor[F] (ce qui a été fait avec listFunctor plus haut, mais nous n’avons ici pas encore de déclaration pour les Option par exemple).

Pour tester notre Functor, définissons à présent une fonction qui prend en paramètre un type de la catégorie Functor sur des Int et transforme ces Int en String en les préfixant par "result: ".

def sayResult[F[_]: Functor](f: F[Int]): F[String] =
  f.fmap(i => "result: " + i.toString)

Utilisons la fonction sayResult sur une liste :

sayResult(List(1, 2, 3))

Ce qui donne, quelque soit la version de Scala et les options utilisées :

List("result: 1", "result: 2", "result: 3")

Function Functor

Passons maintenant à un autre type que les listes : les fonctions.

Je crée une instance de Functor pour les fonctions dont le paramètre d’entrée est fixé. Autrement dit, je considère toutes les fonctions de la forme I => _ comme des objets pour lesquels je m’intéresse uniquement à la valeur de sortie et à sa transformation. Donc dans I => _, je considère que I est fixé. Je cherche du coup à unifier F[_] avec I => _ et je considère que fmap modifie la sortie uniquement. En Scala, on utilise une astuce appelée lambda type pour exprimer qu’on fixe un paramètre et pas l’autre.

implicit def functionFunctor[I] =
  new Functor[({ type l[o] = I => o })#l] {
    override def fmap[A, B](fa: I => A)(f: A => B): I => B =
      { v => f(fa(v)) }
  }

Si vous plissez les yeux suffisamment (;p), vous allez vous rendre compte que fmap ici effectue une simple composition de deux fonctions. { v => f(fa(v)) } compose la fonction I => A avec la fonction A => B, ce qui donne une fonction I => B.

Maintenant, créons une fonction qui prend une String, la convertit en Int et la multiplie par 2. Puis, appliquons dessus la fonction sayResult définie plus haut :

val function: String => Int    = { x => x.toInt * 2 }
val result:   String => String = sayResult(function)
println(result)
println(result("12"))

Sans l’option -Ypartial-unification ou avant Scala 2.12, j’obtiens les erreurs suivantes :

Error:(28, 36) no type parameters for method sayResult: (f: F[Int])(implicit evidence$2: Functor[F])F[String] exist so that it can be applied to arguments (String => Int)
 --- because ---
argument expression's type is not compatible with formal parameter type;
 found   : String => Int
 required: ?F[Int]
    val result: String => String = sayResult(function)
                                   ^
Error:(28, 46) type mismatch;
 found   : String => Int
 required: F[Int]
    val result: String => String = sayResult(function)
                                             ^

Ces erreurs se résument en l’incapacité pour le compilateur Scala d’unifier F[Int] avec le type String => Int (qui est un sucre syntaxique pour Function1[String, Int]). Ces erreurs apparaissent malgré le fait que nous ayons indiqué au compilateur que dans les fonctions nous nous intéressons qu’aux valeurs retournées et pas aux valeurs en entrée.

Depuis Scala 2.12 avec l’option -Ypartial-unification, j’obtiens en sortie :

$$anon$2$$Lambda$9/312116338@1b0375b3 
"result: 24"

Unification entre le monde Scala et Java 8+

L’interopérabilité entre le monde Scala et le monde Java a toujours été une volonté des contributeurs du langage Scala. Elle est aussi souhaitée au niveau de certains frameworks basés sur Scala, comme par exemple Akka ou Spark, qui en plus de proposer une API en Scala proposent aussi une API en Java.

Le projet lambda qui a été intégré dans Java 8 a apporté deux principales nouveautés au niveau du langage : les lambdas expressions et les default methods. Scala 2.12 propose de se rapprocher de ces deux fonctionnalités.

Mais je vous propose de laisser ça pour les prochains articles.