Scala 2.12 : Unification interface et trait

Dans un précédent article, nous avons fait un tour d’horizon des nouvelles fonctionnalités de Scala 2.12. Cette version de Scala propose aussi de favoriser l’interopérabilité avec 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 et recherché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 (JSR 335) 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 propre à Java 8.

Nous allons nous intéresser dans cet article à l’unification entre les interfaces Java 8 et les traits de Scala. Les lambda expressions seront traitées dans le troisième et dernier article sur les nouveautés de Scala 2.12.

Interface et trait

Depuis Java 8, il est possible d’ajouter du code dans les interfaces. Ceci est possible grâce aux default methods qui permettent d’ajouter un comportement par défaut aux méthodes dans les interfaces. Prenons l’exemple d’une interface définissant un message, avec une méthode pour récupérer son contenu et une méthode pour l’afficher.

interface Message {
   String message();
   default void display() { System.out.println(message()); }
}

Cette particularité des interfaces apparue avec Java 8 correspond tout à fait à celui des trait dans Scala. Les traits de Scala correspondent à la définition des traits en sciences informatiques. Notre interface Message peut être traduite en Scala de la manière suivante :

trait Message {
   def message: String
   def display = println(message)
}

Les deux implémentations sont très proches en terme d’écriture. D’une manière générale, l’écriture d’un trait en Scala correspond à la manière d’écrire des interfaces en Java 8, à quelques exception près. Il était donc légitime d’unifier ces deux approches.

Compilation des traits Scala

Avant Java 8, Scala générait pour les trait deux fichiers .class. Par exemple, pour le trait Message, le compilateur Scala générait un fichier Message.class (contenant l’interface Java et la signature des méthodes) et un fichier Message$class.class (contenant une classe abstraite avec l’implémentation en static des méthodes). Actuellement, il ne reste plus qu’un seul fichier généré de cette compilation : Message.class.

Cette unification au niveau Scala permet de gagner un peu d’espace, mais surtout d’avoir une équivalence directe entre Java et Scala entre les interfaces et les traits. Ceci facilite l’utilisation des traits Scala en Java.

Divergence sur la résolution du diamant

Néanmoins, même si cette unification interface / trait est un avantage certain sur le plan de l’interopérabilité, la comparaison s’arrête lors de la résolution de certains problèmes d’héritage, connus sous le nom de problème du diamant. Pour illustrer ce problème, nous allons imaginer trois interfaces / trait : A, ainsi que B et C qui héritent tous les deux de A. Nous allons y ajouter deux classes : D1 qui implémente dans l’ordre B puis C, ainsi que D2 qui implémente C puis B.

En Scala, cela nous donne l’implémentation suivante :

trait A { def message: String }

trait B extends A { override def message = "Message from B" }
trait C extends A { override def message = "Message from C" }

object D1 extends B with C
object D2 extends C with B

println(D1.message)
println(D2.message)
> Message from C 
> Message from B

Il n’y a aucune erreur compilation. Scala met implicitement la priorité au dernier trait intégré. Ce qui suit le sens de la lecture.

En Java, nous obtenons l’implémentation suivante :

interface A { String message(); }

interface B extends A {
   default String message() { return "Message from B"; }
}

interface C extends A {
   default String message() { return "Message from C"; }
}

class D1 implements B, C {}
class D2 implements C, B {}

Le compilateur Java produit des erreurs :

> Error: java: D1 inherits unrelated defaults for message() from types B and C 
> Error: java: D2 inherits unrelated defaults for message() from types C and B

Contrairement à Scala, Java considère que c’est au développeur de résoudre le problème du diamant, en indiquant explicitement la branche qu’il veut suivre.

Conclusion

Ainsi, si unification il y a entre interface Java 8+ et trait Scala 2.12, il ne faut pas oublier qu’il reste des différences de comportement à la compilation.

Nous verrons dans un prochain article qu’il est possible en Scala 2.12 de faire correspondre les lambdas expressions avec n’importe quel type d’interface fonctionnelle, tout comme en Java 8.