Revenons sur les Closures (encore !)

Logo JavaCela fait quelques temps qu’on en parle sur le blog, la première version était loin de mettre tout le monde d’accord. La seconde version a été mieux acceptée, notamment car elle était bien plus simple, puisqu’elle ne correspondait ni plus ni moins qu’à du « sucre syntaxique » pour écrire une classe anonyme (c’est à dire une écriture plus concise). La troisième version des closures paru dans le billet « En Vrac » du 26 octobre a provoqué peu de débat, mais elle revenait à une version plus complexe qui me laissait bien perplexe…

Bref tout cela était un peu flou pour moi, et je ne savais plus que penser de ces « Closures »…

J’ai tout lu ‘Neal Gafter‘ !

Plus exactement, j’ai lu la présentation qu’il a fait sur les Closures lors du dernier Javapolis, ainsi que la dernière version de leurs spécifications (voir le dernier billet « En Vrac » de la rédaction). Enfin je viens également de lire une interview assez récente : Considering Closures for Java.

Et je dois dire, à l’instar de Vincent, que désormais je vois mieux tout l’intérêt de ces closures !

Et je vais essayer de vous présenter cela de manière succincte.

Une syntaxe un peu étrange au premier abord

Les Closures permettent donc de définir des sortes de « fonction » lors de la déclaration de variable, paramètre ou attribut. Ce qui peut prendre la forme suivante :

{ int, int => int }  plus = { int a, int b => a + b };

Notre « type fonction » prend deux paramètres en entrée de type int, et renvoi un int. Son implémentation se contente quand à elle de renvoyer la somme de ses deux paramètres.

Concrètement, le compilateur remplacera ce type de code par la génération d’une interface et d’une classe anonyme, ce qui reviendrait finalement à écrire ceci :


interface Closure1 {
  public int invoke(int i, int b);
}
 
Closure1 plus = new Closure1() {
  public int invoke(int i, int b) {
    return a + b;
  }
};

Note : en réalité l’interface et la classe anonyme générées sont un peu plus complexe car elles utilisent massivement les Generics… mais cela apporte une complexité supplémentaire inutile à la compréhension générale…

Notre « type fonction » étant en réalité un objet comme un autre, on peut l’utiliser tout simplement :

int result = plus.invoke(5, 10);

Qu’est-ce que cela apporte ?

Tel quel, il faut avouer que l’intérêt est assez limité. En effet les closures ne prennent tout leurs sens qu’à partir du moment où ils sont utilisés en paramètre d’une méthode : elles permettent ainsi de passer du code en paramètre.

Actuellement, on utiliserait une interface décrivant une méthode qui sera exécutée, comme c’est le cas par exemple pour la méthode SwingUtilities.invokeLater() qui utilise l’interface Runnable :


  SwingUtilities.invokeLater( new Runnable() {
    public void run() {
      doSomething();
    }
  });

En utilisant les closures, on pourrait utiliser le code suivant nettement moins verbeux :


  SwingUtilities.invokeLater( {=>doSomething()} );

En effet, la conversion des closures en interface/classe anonyme permet de générer une classe anonyme d’un type compatible avec le type requis par les « anciennes » API. Dans ce précis cas le compilateur remplacera la closure par un objet du type Runnable.

Il faut bien sûr respecter un certain nombre de contrainte (l’interface ne doit posséder qu’une seule méthode, les paramètres doivent correspondre, etc.), mais cela permet déjà une simplification de l’écriture des classes anonymes tout en conservant une compatibilité absolu (le compilateur « écrira » la classe anonyme à votre place).

Mais les spécifications vont plus loin, car elle permettent de simplifier encore plus cette écriture afin de lui faire prendre la forme suivante :


  SwingUtilities.invokeLater() {
    doSomething();
  }

On se retrouve ici avec un code nettement plus agréable à lire, qui correspond plus à l’écriture d’un bloc qu’à celle d’une classe anonyme.

Cela pourrait permettre d’étendre les API afin de remplacer certaine structure un peu complexe ou redondante…

Prenons comme exemple l’API de concurrence de Java 5.0. Si elle se révèle très performante et puissante, la syntaxe d’utilisation de ces locks est assez verbeuse :


  Lock l = ...
 
  l.lock();
  try {
    doSomething();
  } finally {
    l.unlock();
  }

Avec les closures il serait possible de grandement simplifier cela :


  Lock l = ...
 
  withLock(l, { => doSomething() });

Ou encore plus directement :


  Lock l = ...
 
  withLock(l) {
    doSomething();
  }

Encore mieux ! Dans cet exemple, le type closure n’utilise aucun paramètre, mais il est bien sûr possible d’en utiliser !

Prenons le cas d’une méthode with() qui utiliserait un Reader en entrée, lequel serait passé à la closure avant d’être proprement fermé (dans un bloc finally par exemple). Son utilisation pourrait donner par exemple :


  with({ Reader in => doSometing(in) }, new FileReader("filename"));

Mais il sera également possible d’utiliser la forme courte en utilisant une syntaxe proche de la boucle for étendu :


  nomDeLaMéthode ( paramètres_de_la_closure : autres_paramètres_de_la_méthode ) {
    code_de_la_closure
  }

Ce qui donnerait :


  with(Reader in : new FileReader("filename")) {
    doSomething(in);
  }

Autre exemple intéressant : le parcours des éléments d’une Map. Actuellement, et malgré l’utilisation de la boucle for étendu cela reste assez contraignant :


  Map<Key,Value> map = ...;
   
  for(Map.Entry<Key,Value> entry : map.entrySet()) {
     
    Key k = entry.getKey();
    Value v = entry.getValue();
     
    // Utilisation de k,v
     
  }

Avec les closures cela pourrait prendre une forme plus sympathique :


  Map<Key,Value> map = ...;
 
  forEach(Key k, Value v : map) {
    // Utilisation de k,v
  }

Ce qui marque dans cette syntaxe, c’est justement qu’elle est vraiment très proche des instruction de contrôle de base (if, for, while).

Et le rapprochement ne s’arrête pas là ! Le code des closures pourra être moins restreint que celui des classes anonymes : les instructions break, continue et return pourront s’appliquer à la méthode appelante, comme s’il s’agissait un simple bloc et non pas de l’appel d’une classe anonyme. De même, le code des closures pourraient directement manipuler des variables du scope parent !

Cela offre la possibilité de définir de nouveau type de bloc, et permettre à chacun d’apporter de nouvelle syntaxe spécifique à chaque API ! Les closures prennent alors toutes leurs ampleurs !

Pour reprendre le dernier exemple, dans le code suivant, l’instruction return dans le code de la closure impliquerait le retour de la méthode getKeyForValue(), et non pas seulement du code de la closure :


  public <K,V> K getKeyForValue(Map<K,V> map, V value) {
    forEach(K k, V v : map) {
      if (value.equals(v)) {
        return k;
      }
    }
    return null; // Valeur non trouvé
  }

Avec une classe anonyme ce type de construction n’est pas possible .

Bien sûr les closures pourront également être utilisée de manière restreinte, c’est à dire qu’elles fonctionneront de la même manière que le code des classes anonymes, ce qui reste obligatoire et/ou appréciable dans certain cas.

Convaincu ?

Personnellement j’ai vraiment hâte de pouvoir jouer avec les closures. Les possibilités me semble vraiment énorme, et cela pourrait être une révolution bien plus importante que ce qu’avait été Tiger en son temps. Cela promet des APIs encore plus poussées et simples !

Enfin, pour tout ceux qui sont intéressés par le sujet, je ne pourrais que leurs conseiller de surveiller le blog de Neal Gafter (si ce n’est pas déjà le cas).

8 réflexions au sujet de « Revenons sur les Closures (encore !) »

  1. professeur shadoko

    à part les vertus des fermetures ma remarque porte sur la syntaxe.
    déjà avec la généricité on a eu ce phénomène de conception et de réalisation intéressante mais s’appuyant sur une syntaxe à ch*** . G.Bracha est très susceptible quand on aborde le sujet et pourtant une recherche sur la lisibilité des expressions serait bienvenue pour accompagner ces recherches sur les fonctionnalités du langage.
    Il est vrai qu’on entre dans le subjectif …. mais pas tant que ça : des expérimentations permettraient de combiner rigueur syntaxique (ce qui n’est dejà pas toujours le cas -l’exemple de la généricité vient encore à l’esprit-), rigueur conceptuelle (là aussi quelques « trous » sont à craindre -toujours exemple de la généricité-) et « ergonomie » du code source … curieusement ce dernier point n’est pas un sujet beaucoup abordé par la recherche.

  2. daedric

    c’est plutot confus pour moi :s si elle sont implementer je m’y mettrais mais perso une possibilite de surcharge d’operateur pour ajouter des Object entre eux ou autre me tente bien plus que ca
    faire des classe ou des interfaces ne me pose pas de probleme ,en plus je trouve ca plus clair ca on a tout le code sous les yeux avec les closure faut se rapporte a la methodes si j’ai bien compris

    enfin article tres interressant ^^
    on vois que le java est le langage le plus dynamique du web ^^

  3. adiguba Auteur de l’article

    >> Pourquoi est-ce qu’il faut remettre le type de a et b alors que l’on a précisé que c’étaient des int ?

    Parce que dans cet exemple on a deux syntaxe qui seront généralement utilisé à des endroits différents.

    Le premier, la définition du « type fonction » sera surtout utilisée en tant que paramètre de méthode :

    public void method( {int, int =&gt; int} );

    La seconde forme sera utilisé lors de la définition du code lui même, généralement lors de l’appel de la méthode :

    method( {int a, int b =&gt; a + b} );

    Et tu est alors obligé de typer les paramètres pour éviter toute ambiguité au compilateur…

    Concernant les propriétés, il y a quelques idées qui trainent (voir le PDF Java 7 du billet en Vrac du 8 décembre).

    Concernant la surcharge d’opérateur je ne suis pas vraiment pour (mis à part pour quelques exceptions comme pour les types numérique : d’ailleur BigDecimal devrait pouvoir en bénéficier) car cela pose souvent plus de problème que cela n’en résoud (le gros problème c’est que le même opérateur peut donner des actions différentes selon les cas).

    a++

  4. alexdp

    Merci pour cet article intéressant (et court, ce qui est tout à son honneur). J’espère ne pas me tromper et il ne faudra pas hésiter à me corriger si c’est le cas : en une phrase, les closures permettent d’enfermer un appel de fonction en lieu et place d’un attribut.

    Evidemment, cela ouvre la porte à l’interfaçage entre classes non compatibles et au compactage de code.

    Concernant le compactage de code, il ne faudra pas tomber dans l’extrême. Je m’aperçoit notamment en production que moins une ligne de code a de resposabilités, plus l’erreur est repérable rapidement.

    Et bien il n’y a plus qu’à se préparer à Java 7 maintenant!

  5. nicØB

    Merci pour ces infos !

    Par contre la syntaxe est parfois lourde pour rien :
    « { int, int => int } plus = { int a, int b => a + b }; »
    Pourquoi est-ce qu’il faut remettre le type de a et b alors que l’on a précisé que c’étaient des int ?

    Il ne manquerait plus que la surcharge des opérateurs et les propriétés pour gagner encore plus en lisibilité …
    Des infos là dessus ? Pour les propriétés, j’ai déjà vu quelques discussions mais pour la surcharge d’opérateurs, niet …

  6. Avatar de lunatixlunatix

    sympa ! on voit que les closures se rafinent de plus en plus, tout en gardant un esprit de simplicité important !

    j’avais pas reflechi non plus que la disponibilité de javac en open source allait permettre de faire plein d’expérimentations et de tests.

Laisser un commentaire