Une Pizza aux 3 langages...

Après avoir lu pas mal d’articles sur Scala et les generics de Java, j’ai essayé de trouver un lien entre tout cela. Pour ce faire, rien de plus simple (!) : prenez quelques tranches de Scala ("high-order functions"), ajouter des bons morceaux de généricité (polymorphisme paramétrique), déposez le tout sur une bonne pâte (programmation fonctionnelle) et introduisez dans un four thermostat 100 (compileur Java). Vous obtiendrez : Pizza !

Plus sérieusement, voici concrètement ce qu’est Pizza, son origine et ses implications dans le monde Java.

Le projet Pizza

"Back to the 90’s". 1995 exactement. Martin Odersky et Niklaus Wirth étudiants à l’Ecole Polytechnique Federale de Lausanne (Suisse), à l’origine des versions 1.1 à 1.4 du compileur Java, décident de travailler sur un langage fonctionnel compilable en bytecode Java. C’est la naissance du projet Pizza. Le but était de créer un langage à la fois objet et fonctionnel, sans les restrictions de Java. Le premier résultat fût Funnel, un langage minimaliste basé sur les "functional nets" mais qui s’est finalement révélé peu utilisable. Scala est donc issu de la seconde mouture de leurs recherches, plus pragmatique et centré sur l’interopérabilité avec les plateformes standards. Il n’est donc pas exactement une extension de Java même s’il lui est totalement compatible et comparable en terme de performances. La première distribution publique de Scala est née en 2003, sa seconde version actuelle est apparue en 2006.

Implications dans le monde Java

Pizza peut donc être vu comme une supercouche de Java (toute application Java est de facto compatible Pizza), lui ajoutant trois principales fonctionnalités souvent réclamées par la communauté : le polymorphisme paramétrique, les fonctions de haut-niveau ("function pointers") et les types de données algébriques ("case classes" et "pattern matching"), sujets sur lesquels je reviendrai plus loin dans cet article. Pizza essaie donc de retranscrire ces concepts dans le monde Java, autant de manière abstraite que concrète, par transposition (adaptation des concepts) ou définition (nouvelles JSR). Comme le dit son concepteur, "Pizza fits smoothly to Java, with only a few rough edges.". C’est notament grace à la JSR-014 que les generics ont fait leur apparition dans Java et qu’ils sont toujours étudiés par l’intermédiaire du compileur étendu de Java : GJ (Generic Java Language).

Détail des concepts majeurs

Le polymorphisme paramétrique

Un exemple simple et rapide pour comprendre de quoi il s’agit (en Java). Le code suivant est syntaxiquement bon :

List<String> lstr = new ArrayList<String>() ;

Alors que celui-ci ne compilera pas :

List<Object> lstr = new ArrayList<String>() ;

En effet, comme nous l’avons vu dans mon précédent article sur les generics, les types paramétrés ne sont pas covariants! C’est donc ceci que veut résoudre Pizza par l’apport du polymorphisme paramétriques : autoriser l’utilisation de l’héritage dans les generics.

Fonctions de haut-niveau

Autrement appelées "Higher-order functions" ou "functions pointer" en C par exemple, elles peuvent être définies simplement comme des functions acceptant d’autres fonctions en paramètre et étant capables de retourner une fonction en résultat. Un exemple simple en Scala :

val numbers = Array(1,2,3,4,5)
val fact = numbers.reduceLeft(_*_)
println("The factorial of five is :" + fact)

Ici, Array.reduceLeft est une méthode de haut-niveau, à laquelle on indique de parser tous les éléments du tableau et de les traiter deux par deux avec la méthode que nous lui passons _*_ qui peut se comprendre comme x*y.

Types algébriques

En termes de programmation fonctionnelle, un type algébrique est un type de donnée qui peut être instancié de plusieurs façons, notamment avec d’autres types disposant du même genre de constructeur et étant des sous-types du premier. Un petit exemple pour éclaircir cette définition. Supposons que nous disposons des classes ci-dessous (Scala) :

abstract class Tree[+T]
case object Leaf extends Tree
case class Node(elem :T, left :Tree[T],right :Tree[T]) extends Tree[T]

Nous pourrons créer la structure suivante :

Grâce à cette "simple" instanciation :

val tree = Node("F", Node("B",Node("A",Leaf,Leaf),Node("D",Node("C",Leaf,Leaf),Node("E",Leaf,Leaf))),Node("G",Leaf,Leaf))

Ces classes sont appelées "case classes" puisqu’elle permettent d’être évaluée avec le mot clé "case" afin de déterminer facilement leur type (par décomposition). Un autre exemple. Soit la classe :

case class Person(firstName :String, lastName :String)

Instanciée de deux manières :

val mark = Person("Mark", "Thomas")
val henry = Person("Henry", "Hudson")

Ces deux instances peuvent faire l’objet d’une évaluation simple avec la syntaxe suivante, pour par exemple rechercher un membre de la famille "Thomas" :

person match {
 case Person(firstName, "Thomas") => println("You are one of my family member "+firstName)
 case _ => println("Nice to meet you" + firstName)
}

Cette méthode est appelée "pattern matching". Un exemple un peu plus concret est donné sur le blog de Martin Odersky lui-même :

try {
  ...
} catch {
  case ex: IOException => "handle io error"
  case ex: ClassCastException => "handle class cast errors"
  case ex: _ => "generic recovery"
}

Nous venons donc de voir en quoi Scala est un langage fonctionnel et quels sont les avantages qu’il apporte à Java en matière d’architecture ou de manipulation des données. Je vous invite à lire les sources ci-dessous car elles apportent une nouvelle façon de voir et de penser complètement différente de la programmation impérative, très utile pour prendre du recul sur sa façon de travailler...

Sources


Fichier(s) joint(s) :

0 commentaires: