10 mai 2007
Imprimer ce billet

Des closures en Java

Le débat autour de l'introduction des closures dans le langage Java fait rage - avec toute la mesure et l'absence de pédanterie dont sait faire preuve notre profession sur ce type de sujet. Selon toute vraisemblance, les closures seront l'une des fonctionnalités phare de Java 7. Reste à savoir sous quelle forme.
Deux écoles ont émergé sur le sujet : la première, désignée BGGA (du nom de ses auteurs et promoteurs, Gilad Bracha, Neal Gafter, James Gosling et Peter von der Ahe), propose une extension syntaxique relativement complexe mais permettant d'introduire dans le langage tous les idiomes nécessaires à un support des closures similaire à celui disponible dans Ruby ou Smalltalk : function types, free variables, blocks, etc. ; la seconde désignée par l'acronyme CICE (pour Concise Inner Class Expressions) et soutenue par Joshua Bloch, Doug Lea et "Crazy Bob" Lee, propose plus modestement une simplification de la syntaxe java dans le but de désinhiber l'usage des Inner Class en lieu et place des closures.
L'occasion de faire ici un point sur ces deux approches et d'apporter, sinon une pierre à l'ouvrage, du moins des éclaircissements sur les termes du débat.

La définition la plus répandue des closures est la suivante (Neal Gafter fournit un historique des closures dans son blog [en]) :

A closure is a function that captures the bindings of free variables in its lexical context.

En substance, une closure est un bloc de code référençable, manipulant optionnellement des variables dites "libres" - libres en ce sens qu'elles sont définies non dans le bloc de code, ni de façon globale, mais par le contexte dans lequel le bloc est exécuté.

Un peu de code valant souvent mieux qu'un long discours, voici un exemple très simple d'utilisation des closures en Ruby (Ruby fait un usage intensif des closures, ce qui est probablement à l'origine de l'affection que lui porte ses utilisateurs). Cet exemple est tiré de l'article de Martin Fowler sur le sujet [en].

Imaginons que vous souhaitiez extraire d'une liste d'employés une sous-liste comprenant uniquement ceux qui sont cadres. Une implémentation possible en java est la suivante :

public static List<Employee> managers(List<Employee> employees) {
  List<Employee> result = new ArrayList<Employee>();
  for (Employee e : employees)
    if (e.isManager) result.add(e);
  return result;
}

En Ruby, la même fonction serait codée de la sorte :

def managers(employees)
  return employees.select {|e| e.isManager}
end

La méthode select est définie dans la classe Collection de Ruby. Elle prend en paramètre un bloc de code - la fameuse closure - , défini entre accolades. Si le bloc de code prend des arguments, ces derniers sont déclarés entre deux barres verticales (ce sont les variables "libres", dont la portée est définie par le contexte). La méthode select encapsule l'algorithmique : itération sur la collection, exécution du bloc de code pour chaque élément et construction d'une sous-liste contenant les éléments pour lesquels le bloc de code s'évalue à true.

Il existe bien sûr en Java un mécanisme assez similaire, autorisé par l'usage de classes anonymes imbriquées (Anonymous Inner Class, ou AIC).
Supposons que la classe List possède, symétriquement à sa contre-partie Ruby, une méthode select, prenant en paramètre une interface définie comme suit :

interface Filter<T>(){
  public boolean accept(T e);
}

Alors le code java pourrait s'écrire comme suit :

public static List<Employee> managers(List<Employee> employees) {
  return employees.select(
    new Filter<Employee> {
      public boolean accept(Employee e){
        return e.isManager();
      }
    }
  );
}

Les AIC sont l'avatar traditionnel des closures en java. Les API du JDK en font largement usage (pensez aux interfaces Runnable, Comparable, Callable ou TimerTask, qui fournissent des closures aux classes Thread ou Executor, ou encore permettent de configurer les collections). Les Design Patterns callbacks, factories, predicates ou stategies sont des candidats naturels à ce type de construction. Spring, en particulier dans son framework de templates, les exploite à très bon escient pour masquer le caractère fastidieux de certaines API, JDBC en particulier.

Pour autant, la programmation par closures, très répandue chez les rubyistes, est quasiment inexistante chez les programmeurs java. Le listing ci-dessus fournit une première explication : la syntaxe des AIC, verbeuse à souhait, est propre à décourager les meilleures intentions. Ensuite, le support des AIC dans Java comme mécanisme de closures souffre de sévères limitations : une fois instanciée, une AIC est un objet à part entière, doté d'une portée qui lui est propre, et partiellement aveugle au contexte qui l'a créé. En conséquence, les variables ou méthodes sont résolues dans le portée de ce nouvel objet.

Examinons, pour illustrer ces quelques lignes de Ruby :

i = 1;
1.upto(100) { |num| i *= num; }
puts i;

Ce code permet d'afficher le factoriel de 100. La variable i est définie en dehors de la closure et modifiée dans son corps. Le code java équivalent ne compilerait pas (à supposer qu'une API similaire soit disponible). En effet, pour qu'une variable externe soit accédée dans le corps d'une AIC, elle doit être déclarée final (la raison sous-jacente est que la variable est copiée dans le contexte de l'AIC). Elle ne peut donc être modifiée. Cela ne pose pas de difficulté si la variable est d'un type mutable (comme une liste ou un wrapper), mais peut se révéler problématique s'il s'agit d'un type immutable ou d'un value type (en particulier les String et les types primaires).

Voici un exemple type des contorsions nécessaires en java pour contourner cette contrainte :

final int[] numCompares = new int[1];
Arrays.sort(a, new Comparator<Integer>() {
  public int compare(Integer i1, Integer i2) {
    numCompares[0]++;
    return i1.compareTo(i2);
  }
});
System.out.println(numCompares[0]);

C'est sur ce constat primordial que la proposition CICE a vu le jour. Son objet n'est pas d'altérer le langage java mais de rendre plus concise la syntaxe de création des AIC (en fait, un sous-ensemble de ces dernières, appelées single-abstract-method-types ou SAM, qui sont en substance des interfaces ne comprenant qu'une unique méthode).

Avec la syntaxe proposée par CICE, notre exemple initial serait réécrit de la sorte :

public static List<Employee> managers(List<Employee> employees) {
  return employees.select(
    Filter<Employee> { return element.isManager();}
  );
}

Cette syntaxe dite concise est complétée par une altération des règles d'accès aux variables locales du bloc appelant (en particulier, celles déclarées explicitement publiques peuvent être assignées dans le corps de l'AIC).

Cette approche a bien sûr le mérite de la simplicité puisqu'ellle n'introduit aucun nouveau concept et ne nécessite pas la réécriture des API existantes ; elle pourrait de surcroît probablement être implémentée à l'aide d'une modification mineure du compilateur.

Pour certains, cependant, l'approche CICE ne fait qu'effleurer le sujet et ne propose qu'une mise à disposition minimaliste des closures en Java.

En effet, d'une façon générale, les AIC rompent le contexte d'exécution du code appelant et ne permettent pas de conserver la sémantique d'un nombre important de structures syntaxiques (celles que Neal Gafter appelle "lexically scoped language constructs") :

  • noms des variables
  • noms des méthodes
  • noms des types
  • signification de this
  • noms des labels
  • référent d'une instruction break sans label
  • référent d'une instruction continue sans label
  • checked exceptions déclarées ou interceptées
  • référent d'une instruction return
  • et quelques autres plus exotiques (état d'assignation des variables, reachability, …)

La proposition BGGA est une extension du langage java permettant la mise en oeuvre des closures sans rupture de transparence. La syntaxe proposée par BGGA est loin d'être triviale, ce qui lui vaut de nombreuses critiques – ses contempteurs sont au demeurant souvent les partisans de CICE.

Sans entrer dans le détail (qui peut être consulté sur le site http://www.javac.info/), on retiendra les caractéristiques suivantes :

  • BGGA définit une syntaxe permettant de déclarer des closures littérales ressemblant à ça :
    {int x, int y> x+y}

  • BGGA définit également le concept des function type, dans lequel une fonction possède une liste d'arguments, un type de retour et une clause throws ; on peut instancier un function type à l'aide d'une closure littérale compatible
  • la transparence lexicale est intégralement garantie, tant pour l'association de variables que pour la résolution de noms ou les structures de contrôle (break, continue, return) – certaines closures peuvent cependant être marquées restricted et se comporter de façon identiques aux AIC
  • BGGA propose une grammaire simplifiée permettant d'exploiter les closures selon une syntaxe très proche de celle des structures de contrôle natives du langage – avec une telle syntaxe, la modification du langage pour introduire la seconde forme de boucle for aurait été superflue

Avec BGGA, l'algorithme suivant

lock.lock();
try {
  ++counter;
}
finally {
  lock.unlock();
}

deviendrait

withLock(lock, {=>
  ++counter;
});

dans la forme canonique et, dans la forme simplifiée :

withLock(lock) {
  ++counter;
}

Notre exemple initial pourrait être codé de la sorte (pour peu que l'API de Collection java se dote d'une méthode select appropriée) :

public static List<Employee> managers(List<Employee> employees) {
  return employees.select({Employee e => e.isManager()});
}

Comme évoqué plus haut, la médaille a un revers. Si côté client l'utilisation des closures BGGA semble relativement simple, la syntaxe risque de considérablement complexifier les API du JDK ou des frameworks désireux d'en tirer parti. Pour se donner une idée, voici le code de la fonction withLock utilisée plus haut :

public static <T,throws E extends Exception>
T withLock(Lock lock, {=>T throws E} block) throws E {
  lock.lock();
  try {
    return block.invoke();
  } finally {
    lock.unlock();
  }
}

A l'instar des génériques, qui ont rendu presqu'illisible au commun le code source de certaines classes du JDK, une telle évolution syntaxique risque de dresser une barrière supplémentaire à l'apprentissage du langage java, et creuser encore davantage le fossé entre l'utilisateur d'API (Joe Java, comme disent certains) et le concepteur d'API. Le jeu en vaut-il la chandelle ?

Références (toutes en anglais) :
[1] La proposition BGGA, présentée par Neal Gafter aux Google TechTalks
[2] L'approche CICE
[3] Un très intéressant échange sur le site de Crazy Bob, partisan de CICE
[4] Deux articles parus sur developerWorks, le premier sur les closures en général, et le second plus spécifiquement sur le débat BGGA vs CICE.
[5] Un article plus ancien, sur l'approche traditionnelle des closures en java

Mots-clefs :, ,