Où va Java ? Les Closures (style BGGA)


Lorsque j’ai commencé à écrire la série de billet « Où va Java ? » conçernant Java 7, j’avais en tête de faire un billet récapitulant les différentes propositions de Closures et leurs spécificités. Malheureusement le temps m’a manqué et je n’ai jamais pu finir ce billet…

Pour rappel, une closures représente un « bloc de code », qui peut être manipuler comme un objet. A l’heure actuelle l’utilisation de classe anonyme est ce qui s’en rapproche le plus (avec une syntaxe assez lourde toutefois). L’objectif des Closures étant d’utiliser une syntaxe plus concise.

Il existe quatre grosses propositions de Closures pour Java :

  • « CICE » (spec), par Bob Lee, Doug Lea et Josh Bloch.
  • « FCM » (spec), par Stephen Colebourne et Stefan Schulz.
  • « C3S » (spec), par Howard Lovatt.
  • « BGGA » (spec), par Gilad Bracha, Neal Gafter, James Gosling et Peter von der Ahé.

Aujourd’hui, et malgré le fait qu’il n’existe pour le moment aucune JSR ni information officiel quand à une éventuelle intégration des Closures dans Java SE 7, cette idée de comparaison me semble bien moins pertinente : la proposition « BGGA » me semble la plus complète. De plus elle a su évoluer afin de s’enrichir des bonnes idées des autres propositions (comme les « Method references » de FCM). Si les Closures se retrouve un jour intégré dans Java, il y a de forte chance que ce soit à partir de cette proposition.

Enfin, « BGGA » propose d’ore et déjà un prototype fonctionnel (closures.tar.gz) qui permet de tester « en vrai » les différentes fonctionnalités de la proposition. Je vais donc me contenter de présenter les Closures façon « BGGA« …

Avant de commencer, je tiens à rappeler qu’il ne s’agit pour le moment que d’une proposition, ce qui fait que ce qui est décrit dans ce billet pourrait évoluer d’ici que les closures soit ajoutés au langage (si c’est le cas un jour). Ce billet a surtout pour objectif de présenter les possibilités que cela pourrait apporter au langage…

Bonne lecture ! (et bon courage cela fait un sacré pavé à lire !)


Sommaire / Accès rapide :


Closures ?

Concrètement, une closure est en fait un objet qui représente une seule et unique méthode nommée « invoke« . Comme toute méthode, cette dernière accepte des paramètres et possède un type de retour, et elle peut éventuellement remonter des exceptions. L’intérêt étant de pouvoir déclarer le tout de manière assez compacte. Ainsi, une closure peut s’apparenter à une classe anonyme.

Par exemple, pour une closure qui permettrait de faire la somme de deux valeurs entières, on utiliserait le code suivant :

{ int x, int y => x + y }

On distingue deux sections, séparé par le signe => :

  • La première partie de l’expression désigne le type et le nom des paramètres, de la même manière qu’on pourrait le faire pour une méthode.
  • La seconde partie contient le code Java de la closures, séparé par des point-virgules lorsqu’il y a plusieurs expressions. L’instruction permet de déterminer le type de retour de la closure (ici une somme de int nous donne un int en retour.

En fait ce code crée une classe qui s’apparenterait à la classe suivante :


new Object() {
  public int invoke(int x, int y) {
    return x + y;
  }
};

Function Types

En plus de cela s’ajoute les « Function Types« , c’est à dire la déclaration du type lui-même, qui peut être utilisée à n’importe quel emplacement où l’on utiliserait des types standards. Son écriture est très proche de celle de la closures en elle-même, mis à part qu’on ne nomme pas les paramètres et que le code Java est remplacé par le type de retour, ce qui donne par exemple :

{ int, int => int } plus;

Ici, on déclare une référence nommée « plus« , qui représente une « function type » prenant deux paramètres de type int, et retournant un int

En fait ce code correspond à la définition d’une interface de la forme suivante :


public interface III {
  public int invoke(int x, int y);
};

Au final la déclaration et l’initialisation d’une closure pourrait ressembler à ceci :

{ int, int => int } plus = { int x, int y => x + y };

On est alors libre de manipuler la référence « plus » comme n’importe quelle référence. On peut donc la passer à d’autre méthode et appeler sa méthode invoke() autant de fois que possible :


int resultat = plus.invoke(3, 2);
System.out.println ( resultat ); // Affiche : 5
System.out.println ( plus.invoke(resultat, 5) );// Affiche : 10

Les closures permettent de passer simplement un bout de code d’une méthode à l’autre, généralement pour effectuer des callbacks ou des traitements spécifiques. Actuellement la solution alternative consiste à utiliser des classes anonymes, ce qui est nettement plus verbeux. A titre d’exemple, la closure ci-dessus pourrait être remplacé par une interface et une classe anonyme de la manière suivante :


interface MaClosure {
  int invoke(int x, int y);
};

MaClosure plus = new MaClosure() {
  public int invoke(int x, int y) {
    return x + y;
  }
};

En clair, les « Function Types » correspondent à la déclaration de l’interface Java associé à la closure, alors que la closure en elle même correspond à une implémentation concrète.

Les « Function Types » et les closures peuvent bien sûr prendre plusieurs forme, et remonter des exceptions, par exemple :


// Définition d'une "function type" prenant un double en paramètre,
// et retournant une String
// Implémentation d'une "closure" qui formate le double en String
{ double => String } format = { double d => String.format("%.2f", d) };
 
// Définition d'une "function type" prenant un int en paramètre,
// et pouvant remonter une exception
// Implémentation d'une "closure" qui fait une pause de 'n' secondes
{ int => void throws Exception} sleep = { int n => Thread.sleep(n*1000); };
 
// Définition d'une "function type" pouvant remonter une exception
// Implémentation d'une "closure" remontant une exception dans tous les cas
{ => void throws IOException } boom = { => throw new IOException("Boom"); };
 
// Définition d'une "function type" sans paramètre ni type de retour
// Implémentation d'une "closure" qui affiche un "Hello World !"
{ => void } hello = { => System.out.println("Hello World !"); };
 
// Définition d'une "function type" qui retourne une valeur entière  
// Implémentation d'une "closure" retourne un nombre entre 0 et 100
{ => int } number = { => (int)(Math.random()*100.0) };

On remarquera qu’on n’utilise pas le mot-clé return pour renvoyer la valeur de retour. La valeur de retour de la closure correspond en fait à la dernière instruction effectué par la closure sans point-virgule final. Cela s’explique par le fait que le mot-clé return pourra être utilisé pour interagir directement avec la méthode appelante (voir plus loin les « unrestricted closures »).

On retrouve également la notion de covariance sur le type de retour apparut dans Java 5.0, qui permet de spécifier plus précisément le type de retour dans une classe fille ou une implémentation (ici la closures est une classe implémentant l’interface représentée par la « function type ») :


// Définition d'une "function type" qui retourne un Number
// Implémentation d'une "closure" qui retourne un Double
{ => Number } number = { => 10.0 };

Comme Double est un type fils de Number, le code est parfaitement valable. L’implémentation de la closure est juste plus précise que la définition de la « function type », mais les types sont compatible. En clair le type de retour de la closure doit être le même type ou un type fils de celui déclaré dans la « function type ».

Cela ne concerne bien sûr que le type de retour. Les paramètres des closures prennent quand à eux en compte la « contravariance », le concept inverse de la covariance : afin de respecter le contrat des paramètres de la « function type », les paramètres de la closures doivent correspondre au même type ou à un des types parents, afin que l’appel puisse être effectuer sans erreur. Par exemple :


// Définition d'une "function type" qui prend une Date en paramètre
// Implémentation d'une "closure" qui affiche un objet
{ Date => void } show = { Object o => System.out.println(o); };

Puisque Object est un type parent de Date, le contrat est donc correct puisqu’en utilisant des Dates on pourra très bien utiliser l’implémentation de la closure. L’inverse n’aurait bien sûr pas été correct.

Tout cela peut étrange et pas très lisible de premier abord, et on pourrait croire qu’il s’agit de simple sucre syntaxique en remplacement des classes anonymes. Mais il s’agit bien plus que de cela, et cela ouvre la porte à de nombreuses nouvelles possibilitées.

Accès aux variables locales

Les closures apportent avec elles une autres nouveautés assez importante : l’accès aux variables locales. Actuellement une classe anonyme ne peut utiliser que partiellement les variables locales de la méthode parente. En fait elle ne peut utiliser que les variables déclarées en final. Cette restriction s’impose du fait de la gestion de la mémoire des variables locales (et des paramètres de méthodes). En effets ces derniers sont conservées dans le stack créées lors de l’appel de la méthode, et sont donc détruit à la fin de la méthode. Les références ne sont donc valables que pendant la durée de vie de la méthode, mais le code des classes anonymes pourraient très bien s’exécuter plus tard, voir dans un autre thread, et donc référencer une variable inexistante. C’est pourquoi il est interdit de référencer une variable locale non-final dans une classe anonyme.

Le mot-clé final permettant d’empêcher toute modification de la référence, les classes anonymes utilisent alors une copie de la référence (ou de la valeur du type primitif).

Toutefois, cela entraîne un certain nombre de limitation…

La spécification des closures instaure la possibilité de modifier les variables locales de la méthode appelante, en utilisant l’annotation @Shared qui indique que la variable locale est « partagée » avec au moins une des closures (ou classes anonymes) définies dans la méthode. De ce fait la variable locale n’est plus stocké dans le stack de la méthode, mais dans le « heap ». Le heap est l’espace de stockage chargé de stocker les objets, dont la durée de vie n’est pas forcément limité à la durée d’une méthode. La durée de vie de ces variables locales partagées est donc potentiellement allongée selon la durée de vie des classes anonymes ou closures qui les utilisent.

Ainsi, il est tout à fait possible d’utiliser et de modifier une variable locale de la méthode parente dans une closure ou une classe anonyme :


@Shared int count = 0;
 
{ => void } print = { => System.out.println( count++ ); };
 
print.invoke(); // Affiche : 0
print.invoke(); // Affiche : 1
print.invoke(); // Affiche : 2
print.invoke(); // Affiche : 3
 
System.out.println("count = " + count);  // Affiche : count = 4

Cela comble un des défauts majeurs des classes anonymes, qui implique parfois quelques bidouilles pour effectuer des traitements assez banal (en général on utilise alors un tableau à une dimension).

Closures Conversions

Le second aspect intéressant des closures vient du fait qu’elles sont compatible avec toutes les interfaces mono-méthodes, c’est à dire les interfaces ne définissant qu’une seule et unique méthode. Il n’est donc pas obligatoire d’utiliser une « function type » afin de pouvoir définir et utiliser la closure.

Prenons notre premier exemple qui se contentait de sommer deux int :

{ int, int => int } plus = { int x, int y => x + y };

On pourrait très bien utiliser une interface standard à la place de la « function type » :


public interface IntPlus {
  int add(int x, int y);
}
IntPlus plus = { int x, int y => x + y };

Et la closure est alors automatiquement convertie en une implémentation de l’interface, et le code de la closure sera alors accessible via la méthode add() défini par l’interface.

Cela peut paraître inutile de prime abord, mais cela permet en fait d’utiliser les closures avec des APIs existantes qui utilise d’ores et déjà des interfaces mono-méthodes (et mine de rien il y en a beaucoup).

Prenons la méthode Collections.sort() qui accepte un Comparator afin de trier une liste selon un ordre spécifique. Par exemple pour trier une liste de String en ignorant la casse, on utiliserait actuellement le code suivant :


Collections.sort(list, new Comparator<String>() {
  public int compare(String s1, String s2) {
    return s1.compareToIgnoreCase(s2);
  }
});

Avec la conversion des closures, on peut utiliser directement ceci :


Collections.sort(list, { String s1, String s2 => s1.compareToIgnoreCase(s2) } );

Autre exemple : l’utilisation d’invokeLater(), très chère aux développeurs Swing :


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

Qui pourrait s’écrire de la manière suivante :


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

De ce fait, les closures pourront être largement exploitées sans avoir à modifier les APIs existantes, ni casser la compatibilité ascendante.

Note : les plus attentifs auront peut-être remarqué que l’interface Comparator n’est pas mono-méthode puisqu’elle déclare en fait deux méthodes : compare(int,int) et equals(Object). L’astuce vient du fait que le compilateur ne prend pas en compte les méthodes compatible avec les méthodes déjà existantes dans la classe de base Object, comme c’est le cas de equals(Object)

Method references

On a vu qu’une closure pouvait être considéré comme une classe représentant une méthode. Les « method references » vont encore plus loin dans le concept en permettant de référencer directement une méthode en tant que closure, en utilisant une syntaxe proche de celle utilisée dans la javadoc :

        Nom # identifier ( TypeList )

Où :

  • Nom correspond soit à un nom de classe, soit à une référence valide.
  • identifier correspond au nom de la méthode.
  • TypeList correspond à la liste des paramètres de la méthode.

Pour commencer simplement, prenons la métode static JOptionPane.showMessageDialog(Component, Object). Sa « method reference » correspond tout simplement à ceci :


JOptionPane#showMessageDialog(Component, Object)

L’idée étant qu’une référence de méthode est automatiquement convertie en une closure dont les paramètres et le type de retour sont compatible avec ceux de la méthode, ce qui nous donnerait dans ce cas précis :


{ Component, Object => void } showMessage = JOptionPane#showMessageDialog(Component, Object);

(bien entendu on applique les mêmes règles de covariance et de contravariance que pour les closures standard).

En ce qui concerne les méthodes d’instances (non-static), on se retrouve avec deux cas distinct, selon qu’on utilise le nom de la classe ou la référence à une instance. Prenons comme exemple la méthode String.compareTo(String).

En utilisant une instance dans la « method reference », on obtient une closure avec une signature identique à la méthode, et qui utilisera la référence indiqué à chaque appel de sa méthode invoke() :


{ String => int } compareToHello = hello#compareTo(String);
// Les deux lignes suivantes sont désormais équivalentes :
r = compareToHello("a");
r = hello.compareTo("a");

A l’inverse, si on spécifie le nom de la classe (comme pour une méthode static), on n’a plus aucune instance à utiliser pour appeler la méthode, et la closure se voit alors enrichie d’un paramètre supplémentaire au tout début de la liste des paramètres, et qui devra contenir l’instance sur laquelle la méthode sera appliquée, ce qui nous donne :


{ String, String => int } compare = String#compareTo(String);
// Les deux lignes suivantes sont désormais équivalentes :
r = compare("a", "b");
r = "a".compareTo("b");

De plus, comme une « method reference » est une closure, on peut également utiliser la conversion de closure pour associer une méthode à une interface mono-méthode, de ce fait on peut désormais passer une méthode comme argument d’une autre méthode, et ce très simplement.

Prenons l’exemple des ActionListener des composants Swing. Personnellement j’ai pris l’habitude d’écrire une méthode pour chacun de mes traitements, et les différents listeners se contentent de l’appeler :


private void doSomething(ActionEvent e) {
  // Traitement
}

Ce qui dans l’état actuel me donne le code suivant :


button.addActionListener(new ActionListener() {
  public void actionPerformed(ActionEvent e) {
    doSomething(e);
  }
});

Avec les closures on pourrait déjà utiliser ceci :


button.addActionListener( { ActionEvent e => doSomething(e); } );

Ou encore plus simplement avec une « method reference » :


button.addActionListener( this#doSomething(ActionEvent) );

Clair, net et précis !

Unrestricted Closures

Nous venons donc de voir les concepts de bases des closures :

  • Les « Function Types » qui définissent l’interface représentant une closure.
  • Les « Closures » en elle même qui représentent une implémentation.
  • L’accès étendus aux variables locales.
  • La convertion des closures qui permet de les transformer en une implémentation d’une interface « mono-méthode ».
  • Les « Method references » qui permettent de gérer une closure représentant une méthode existante.

Toutes ces notions se retrouvaient plus ou moins dans les autres propositions de closures, dans des formes plus ou moins proche et plus ou moins complète, et l’objectif principale étant bel et bien de simplifier l’écriture des classes anonymes.

Mais la proposition « BGGA » va un peu plus loin avec les « unrestricteds closures » qui permettent carrément de définir de nouvelles structures de contrôle. Qu’est-ce qu’une « unrestricteds closures » ? Il s’agit en fait d’une closure qui conserve un lien très fort avec le bloc de code depuis laquelle elle est déclarée, si bien qu’elle peut intéragir avec lui. Ainsi, une « unrestricted closure » peut utiliser les mots-clés break, continue ou return, qui interragiront avec le bloc parent de la closure. C’est à dire que si on fait un break ou continue dans la closure, cela impactera la boucle de la méthode contenant la closure. De même si on fait un return, ce sera la méthode parente qui retournera la valeur et non pas la closure. De plus, elles peuvent accéder aux variables locales du bloc même si elles ne sont pas annoté par @Shared. En fait elles ont les mêmes possibilitées qu’un bloc de code basique…

Une « unrestricted closure » se distingue des closures « standards » par le fait qu’elle utilise une double flèches : ==>

Par exemple, voici une boucle infini qui exécute une « unrestricted closure » à chaque iteration. Le break à l’intérieur de la closure permet de stopper aléatoirement le while à l’extérieur de la closure :


while (true) {
  { ==>
    if (Math.random()<0.2) break;
    System.out.println("Essayes encore");
  }.invoke();
}

Cet exemple peut sembler complètement inutile (il l’est !), mais il permet de mettre en évidence l’interaction entre l’unrestricted closure et le bloc de code le contenant (le while en l’occurence). Du fait de ce lien très fort, les « unrestricted closures » doivent être exécuté dans le bloc où elles ont été déclarées, sous peine de provoquer des erreurs à l’exécution si le contexte d’exécution est différent. Impossible d’exécuter une unrestricted closure dans un autre thread ou en dehors du while dans ce cas : cela n’aurait plus aucun sens.

De ce fait les « unrestricted closures » seront généralement exécutées dès leurs créations, pour une raison bien simple : il seront en fait utilisé comme structures de contrôle.

Structures de contrôles

Prenons comme exemple la méthode suivante, qui attend en paramètre un objet Lock et une unrestricted closure :


public static void synchronizedWith(Lock lock, {==>void} code) {
  lock.lock();
  try {
    code.invoke();
  } finally {
    lock.unlock();
  }
}

Cette méthode s’occupe en fait d’acquérir et de libérer le lock afin d’exécuter le code de la closure en possédant le verrou. On appellerait donc cette méthode de la manière suivante :


Lock lock = new ReentrantLock();
 
synchronizedWith(lock, {==> System.out.println("Synchronized !"); });

On arrive alors sur l’autre particularité des « unrestricted closures » : lorsqu’elles sont utilisées comme dernier paramètre d’une méthode, il est possible de simplifié leurs écritures et de s’approcher de la syntaxe des structures de contrôle.

En effet, lorsque une unrestricted closure correspond au dernier paramètre de la méthode, on peut la « sortir » de la liste des paramètres afin d’obtenir quelque chose qui ressemble plus aux structures de contrôles traditionnelles (for, while, if, synchronized, …), et qui donnerait dans notre cas :


synchronizedWith(lock) {
  System.out.println("Synchronized !");
}

On comprend mieux l’intérêt au fait que les mots-clés break, continue ou return puissent impacté le code appelant : la closure est vu comme un bloc de code quelquonque. Les unrestricted closures, couplées à la définition de nouvelle « structure de contrôle », permettent donc de remplacer certains patterns assez « lourd » par une structure plus légère et bien plus simple.

On remplace donc le pattern d’acquisition/libération de verrou assez lourd syntaxiquement (à base de try/finally), et on permet d’utiliser la nouvelle API de concurrence de Java 5.0 avec une syntaxe très proche de celle du mot-clé synchronized.

Très proche… mais pas encore parfait !

Transparence des exceptions et de la valeur de retour

J’ai un peu triché : mon exemple ne prend pas en compte les éventuelles exceptions qui pourrait être remontées par le bloc de code. Ainsi le code suivant ne compile pas et oblige à utiliser :


try {
  synchronizedWith(lock) {
    if (unTest) {
      throw new MyException("Boum");
    }
  }
} catch (MyException e) {
  e.printStackTrace();
}

En effet, le code de la closure ne déclare aucune exception, et la méthode synchronizedWith() ne les gère pas, ce qui provoque une erreur de compilation.

Pour pallier à ce problème, un mécanisme de complétion transparente a été mis en place via les Generics. Grosso-modo en déclarant une exception générique qui serait remonté à la fois par la closure et par la méthode, on permet au compilateur de laisser passer les exceptions dynamiquement selon le code utilisé. La définition de synchronizedWith() deviendrait alors :


public static <throws E> void synchronizedWith(Lock lock, {==>void throws E} code) throws E {
  lock.lock();
  try {
    code.invoke();
  } finally {
    lock.unlock();
  }
}

La déclaration de l’exception générique est répété dans la déclaration de la closure et dans la déclaration de la méthode, ce qui permet au compilateur de permettre à la méthode de remonter dynamiquement la même exception que celles définis dans la closure lors de l’utilisation du bloc de contrôle. Le type réel de E est déterminé par le compilateur lors de la compilation selon le type réel des exceptions remontées par la closure.

Ce mécanisme gère de manière transparente l’absence d’exception : c’est à dire que si la closure ne remonte aucune exception, la méthode ne renverra aucune exception.

Dès lors, on se retrouve exactement dans le même cas d’utilisation que l’instruction finally : une exception quitte le bloc et libère le verrou !

Un mécanisme similaire permet d’associer le type de retour de la closure à celui de la méthode, en définissant un type générique commun :


public static <T, throws E> T synchronizedWith(Lock lock, {==>T throws E} code) throws E {
  lock.lock();
  try {
    return code.invoke();
  } finally {
    lock.unlock();
  }
}

Encore une fois, le type de retour déclarer dans les Generics se retrouve répéter dans le retour de la méthode et de la closure afin de les associer. Le compilateur saura traiter l’absence de type de retour et bien le traiter comme un void sans générer d’erreur.

Toutefois, si la méthode renvoi une valeur, on ne pourra pas utiliser la syntaxe sous forme de structure de contrôle et on se limitera alors à une simple closure :


int x = synchronizedWith(lock, {==> value + 1 });

Pourquoi définir de nouvelles structures de contrôle ? Cela peut paraître inutile de premier abord, mais lorsqu’on y regarde de plus près on s’aperçoit qu’il existe un grand nombre de code répétitif qui pourrait être intégré dans une structure de contrôle. En plus du synchronizedWith(), je vois d’autres structures de contrôle qui pourrait s’avérer utile :

  • log(Level) {…}, qui exécuterait le bloc selon le niveau de log de l’application.
  • nocatch() {…}, qui permettrait d’ignorer les checked-exceptions (et qui les convertirait à la volée en RuntimeException).
  • time() {…}, qui calculerait le temps d’exécution d’une portion de code.
  • transaction(Connection) {…}, qui s’occuperait de faire le commit ou le rollback des données selon le bon déroulement des requêtes SQL.
  • Et bien sûr un with() {…} qui s’occuperait de gérer proprement la libération des ressources dans un try/finally, ce qui est impératif pour un code propre, mais malheureusement rarement fait par grand nombre de développeurs Java…

Structures de contrôle étendus (avec paramètres de closures)

Je vais justement prendre l’exemple de la libération des ressources, en définissant une structure de contrôle permettant d’utiliser simplement un objet Closeable. Une telle structure de contrôle serait finalement assez simple à mettre en place :


public static <C extends Closeable, throws E>
void with(C flux, {C==>void throws E} code) throws IOException, E {
  // Le flux est créé juste avant l'appel et reçu en paramètre
  try {
    // On exécute le code de la closure associée
    code.invoke(flux);
  } finally {
    // Puis on ferme le flux dans tous les cas
    flux.close();  // throws IOException
  }
}

Comme cette « unrestricted closure » prend un paramètre en entrée, cela impacte sa représentation en structure de contrôle : la déclaration des paramètres de la closure s’effectue avant les paramètres de la méthode, en séparant les deux par « deux-points » (dans le style du for étendus de Java 5.0). Cela nous donne donc ceci :


with(FileInputStream in : new FileInputStream("x")) {
  /* code */;
}
 
// Ce qui est strictement équivalent à :
 
with(new FileInputStream("x"), {FileInputStream in ==> /* code */;} );

Et puisqu’un bon exemple vaut tous les discours, je me contenterais de comparer l’utilisation de cette nouvelle structure avec ce qui se fait aujourd’hui. Prenons l’exemple d’une méthode qui effectue une copie de fichier manuellement. Nous avons donc deux flux à fermer et donc deux try/finally.

Actuellement, pour faire cela proprement nous devrions faire ceci :


public static void copy(File inFile, File outFile) throws IOException {
 
  FileInputStream in = new FileInputStream(inFile);
  try {
    FileOutputStream out = new FileOutputStream(outFile);
    try {
      byte[] buf = new byte[1024];
      int len;
     
      // copy
      while ( (len=in.read(buf)) >= 0 ) {
        out.write(buf, 0, len);
      }
    } finally {
      out.close();
    }
  } finally {
    in.close();
  }
}

Avec la structure de contrôle défini ci-dessus, on pourrait simplement faire cela :


public static void copy(File inFile, File outFile) throws IOException {
 
  with(FileInputStream in : new FileInputStream(inFile))
  with(FileOutputStream out : new FileOutputStream(outFile)) {
    byte[] buf = new byte[1024];
    int len;
       
    // copy
    while ( (len=in.read(buf)) >= 0 ) {
      out.write(buf, 0, len);
    }
  }
}

On se concentre sur notre traitement tout en utilisant une libération propre des ressources !

Structures de contrôle de boucle

Mais pour aller encore plus loin, les spécifications des closures BGGA intègre un mécanisme permettant de simuler la mise en place de structures de contrôle de boucle.

Nous avant vu qu’une unrestricted closure pouvait utiliser les mots-clés break et continue, mais que ceux-ci s’appliquaient à la boucle englobant la déclaration de la closure. En définissant une structure de contrôle de boucle, on « marque » notre structure comme une boucle, et les mots-clés break et continue s’appliqueront alors sur notre structures et non pas sur une boucle englobante. Il suffit pour cela d’utiliser le mot-clé for lors de la déclaration de la méthode.

Par exemple, une structure « équivalente » au for étendu de Java 5.0 pourrait s’ecrire de la manière suivante :


public static for <T, throws E>
void each(Iterable<T> iterable, {T==>void throws E} code) throws E {
  Iterator<T> iterator = iterable.iterator();
  while (iterator.hasNext()) {
    code.invoke(iterator.next());
  }
}

On remarque le mot-clé for dans la signature de la méthode, qui indique que la méthode est une structure de contrôle de boucle, et qu’elle doit donc également être utilisé de la sorte en utilisant le mot-clé for lors de l’appel de la méthode, par exemple :


List<String> list = ...
 
// La syntaxe d'appel est proche de celle du for étendu de Java 5.0 :
for each(String str : list) {
  System.out.println(str);
}

De ce fait, désormais les mots-clés break et continue s’applique sur notre structure de contrôle et non pas sur la structure l’englobant. Ainsi la présente d’un break mettra un terme à l’appel de la méthode each(), et donc à notre boucle.

Quel intérêt de reproduire une structure de boucle ?
Le premier intérêt serait d’étendre le « for étendu » de Java 5.0 à d’autre type non supporté, comme Iterator :


public static for <T, throws E>
void each(Iterator<T> iterator, {T==>void throws E} code) throws E {
  while (iterator.hasNext()) {
    code.invoke(iterator.next());
  }
}

Ou encore l’antique Enumeration toujours bien présente dans l’API :


public static for <T, throws E>
void each(Enumeration<T> enumeration, {T==>void throws E} code) throws E {
    while (enumeration.hasMoreElements()) {
        code.invoke(enumeration.nextElement());
    }
}

Cela nous permettrait également de simplifier des boucles plus complexe, comme le parcours des Map par exemple :


public static for <K,V> void each(Map<K,V> map, {K,V==>void} code) {
    for (Map.Entry<K,V> entry : map.entrySet()) {
        code.invoke(entry.getKey(), entry.getValue());
    }
}

Qui s’utiliserait de la manière suivante (notez bien les deux paramètres avant le « : » :


for each(Integer key, String value : map) {
    System.out.println(key + " = " + value);
}

A titre d’information actuellement le parcours de boucle s’effectue via un code du style :


for (Map.Entry<Integer,String> entry : map.entrySet()) {
    Integer key = entry.getKey();
    String value = entry.getValue();
    System.out.println(key + " = " + value);
}

Tout comme les structures de contrôles, on pourrait imaginer un grand nombre de boucle simplifiée de ce genre, qui nous épargnerait l’utilisation de code courant mais syntaxiquement assez lourd.

  • for lines(String:File), qui parcourerait les lignes d’un fichier (ou d’un flux).
  • for pool(E:Queue<E>), qui parcourerait les éléments disponible dans une Queue.
  • for take(E:BlockingQueue<E>), qui parcourerait tous les éléments dans une BlockingQueue, en bloquant le thread courant si aucun élément n’est disponible.
  • for files(File:File), qui permettrait de parcourir les fichiers d’un répertoire sans créer de tableau temporaire (afin de parcourir plus rapidement les répertoires contenant de multiples fichiers).
  • for concurrent(), qui permettrait de profiter des avantages des processeurs multicores en séparant les itérations sur divers threads. Bien sûr il faudrait dans ce cas redéfinir le comportement des mots-clés continue (qui ignorerait simplement le traitement courant), et break (qui devra interrompre les autres threads).

Conclusion

Il y aurait encore beaucoup à dire sur les closures…

Pour commencer, je dois avouer que je ne suis pas particulièrement fan de la syntaxe, et que de ce coté là je suis plus proche de celle opté par la proposition FCM. A titre d’exemple, si on reprend le tout premier exemple de cet article :


{ int, int => int } plus = { int x, int y => x + y };

On obtiendrait avec FCM la syntaxe suivante, qui me semble plus proche de la déclaration des méthodes :


#(int(int,int)) plus = #(int x, int y) { return x + y; }

Mais la syntaxe n’est pas le plus important : cela nécessitera une adaptation certaine dans tous les cas. L’intérêt réside surtout dans les possibilité que cela apporte, et de ce coté là on est servi :

  • L’écriture simplifié en comparaison des classes anonymes !
  • L’accès simplifié aux variables locales (via @Shared), qui est un plus non négligeable, et qui bénéficiera même aux classes anonymes.
  • La convertion des closures en interface selon le contexte, ce qui permet d’utiliser des closures à la place des trop verbeuses classes anonymes.
  • Les références de méthode et leurs convertions en closure (même si cela ne va pas aussi loin que dans la proposition des FCM).
  • Et surtout (en comparaison des autres propositions de closures), la possibilitée de définir des structures de contrôle, et donc de permettre aux APIs de proposer de nouvelles structures sans que cela ne doivent impacter les spécifications du langage. Une structure simple est bien plus efficace et propre que n’importe quelles « bonnes pratiques » qui sont souvent « oublié » lors du développement :(

A l’inverse, les principaux défauts que je vois viendrait surtout d’une utilisation abusives des functions types ou des structures de contrôle, ce qui complexifierait le code au lieu de le simplifier…

Je regrette simplement l’absence de JSR, que ce soit pour les closures ou pour Java 7, ce qui fait que l’on reste dans le flou le plus complet quand à une intégration ou non des closures dans le futur. De mon point de vue, c’est une des fonctionnalités manquantes du langage.


Où va Java ? Les Closures

16 réflexions au sujet de « Où va Java ? Les Closures (style BGGA) »

  1. Avatar de AlpAlp

    Excellent billet adiGuba !

    Très intéressant, et ce n’est pas étonnant de voir Java s’attaquer à ça.

    Mais comme dans à peu près tous les langages impératifs/OO qui s’attaquent au fonctionnel, la syntaxe est assez lourde et les facilités mises en oeuvres ne sont pas si faciles que ça.

    Mon code fonctionnel restera en OCaml, +1 à SpiceGuid :)

  2. Avatar de UtherUther

    A mon avis ton mix n’est pas une bonne idée, ajouter de nouvelles notations et wildcard est a éviter le plus possible, même si dans le cas des closures c’est à mon avis necessaire. Or là tu ajoute deux notation vraiment différentes qui recourent à la fois à # et => entre la closure et la fonction type.

    Pour le problème du for, l’inversion ne me pose pas de problème. Ca permet d’être totalement consistant avec la syntaxe du for étendu de java 5:
    for [closure](déclarations des variables gérées : variables traitées) { … }

    Je ne connait pas trop les closures des autre langages. Qu’est ce que tu appelles closure généralisé?

    Pour les syntaxes je suis plutôt pour FCM. BGGA me parait plus avancé au niveau des possibilités que les autres propositions, mais je suis d’accord que la syntaxe FCM est plus claire.
    Les syntaxes CICE et C3S, on l’avantage d’être moins choquantes que FCM ou BGGA mais elles ne me paraissent pas vraiment adaptables à toutes les possibilité offertes par BGGA.
    Par contre comme dit adiguba, la syntaxe FCM pose quand même quelques questions si on veut l’utiliser avec les fonctionnalité de BGGA comme : que faire du return, des unrestricted closures,… .

  3. bredelet

    Moi je verrais bien un mix des syntaxes

    { int, int =&gt; int } plus = { int x, int y =&gt; x + y };&nbsp;<br />
    #(int(int,int)) plus = #(int x, int y) { return x + y; }

    Proposition:

    #(int(int,int)) plus = { int x, int y =&gt; x + y };

    De plus l’interversion des parametres du for est un peu tiree par les cheveux, pourquoi ne pas les laisser a leur place:

    for each(map) { Integer key, String value ==&gt;&nbsp;<br />
    &nbsp;   System.out.println(key + " = " + value); &nbsp;<br />
    }

    Tu as explique pour les closures standard, mais les closures generalisees ne peuvent pas s’implementer en terme de classe anonyme avec une methode invoke. Alors comment ca marche?

  4. Avatar de UtherUther

    Certes mais un

    onEdt(){ ... }

    ça fait tellement plus simple et clair qu’un

    SwingUtilities.invokeLater({=&gt; ... });

    Cette syntaxe abstrait totalement les problématiques de closures et d’inner classes anonymes. J’espère que l’API swing fournira quelquechose du même style pour Java 7.

    Ps: Il n’y a aucun moyen d’éditer les commentaires dans les blogs? je n’ai pas pensé à verifier si PRE faisait partie des balises XHTML authorisées pour mon post précédent :(

  5. adiguba Auteur de l’article

    Uther : a noter que l’on aurait également pu faire ceci :

    &nbsp;<br />
    TestBGGA mainWin = new TestBGGA();&nbsp;<br />
    &nbsp;<br />
    SwingUtilities.invokeLater({=&gt;&nbsp;<br />
    &nbsp;   mainWin.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);&nbsp;<br />
    &nbsp;   mainWin.setSize(640,480);&nbsp;<br />
    &nbsp;   mainWin.setVisible(true);&nbsp;<br />
    });

    a++

  6. Avatar de UtherUther

    Je viens de tester le prototype et j’ai testé rapidement un petit bout de code qui risque de me servir souvent a l’avenir
    import javax.swing.*;

    class TestBGGA extends JFrame{
    public static void main(String args[]){
    TestBGGA mainWin = new TestBGGA();
    onEdt(){
    mainWin.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
    mainWin.setSize(640,480);
    mainWin.setVisible(true);
    };
    }

    public static void onEdt({==>void} code){
    SwingUtilities.invokeLater(new Runnable(){
    public void run(){
    code.invoke();
    }
    });
    }
    }

    Ca a l’air de marche plutôt bien.

  7. adiguba Auteur de l’article

    Uther : Oui le @Shared m’a un peu surpris à moi aussi, puisque une annotation n’a aucun impact sur le code généré…
    En réalité il n’est pas obligatoire d’utiliser l’annotation : le compilateur détectera tout seul les variables « partagées ». L’annotation est surtout présente pour le signaler aux développeurs (en fait par défaut on obtient un warning si on oublie l’annotation).

    Pour info, dans ce même cas de figure la proposition CICE a opté pour le mot-clé public : on déclare que la variable est accessible par les inner-classes et closures.

    Maintenant le tout est de savoir où se situe la limite entre un mot-clé et une annotation…

    a++

    PS : J’ai corrigé les deux erreurs. Merci ;)

  8. Avatar de UtherUther

    Bravo pour cet article trés complet et clair. Je m’étais renseigné sur les closures il y a un moment déjà, mais soit j’ai trouvé des articles qui n’expliquaient que le cas de base, soit il s’agissait d’explications relativement complexe et je décrochais. Tu présente clairement toutes les possibilité et met bien en avant les avantages

    Je ne m’étais pas rendu compte de toute les possibiltés offertes par cet ajout, notamment les fonctions types et les structures de controle. C’est sur que si c’est bien intégré dans l’API y compris dans l’existant, ca risque de changer bien des chose à nos habitudes.

    J’aurais tout une remarque/question:
    Pourquoi @Shared devrait être une annotation alors qu’il change le comportement même de la variable? L’inpact est a mon avis bien supérieur à trasient, et comparable à strictfp et volatile, … qui sont pourtant des mots clé.

    Enfin quelques petites erreur a corriger:
    - Il traine un « &amp;nbsp; » parasite dans le premier code de la partie structure de controle
    - Dans le structures de controle utiles que tu propose tu parles de width() {…}, pour la libération automatique de ressources. Je suppose que tu veux parler de with() {…}

  9. adiguba Auteur de l’article

    Je ne suis pas sûr que les Generics puissent être utilisés directement dans une fonction type comme dans cet exemple (il sont utilisable seulement si la closure est un paramètre d’un méthode elle-même paramétré, et dans ce cas on utilise les type Generics de la méthode.

    De toute manière comme les fonctions type sont déclarées en ligne on pourrait l’adapter très facilement, par exemple si on a f(double,int) :

    {int, double =&gt;void} invert = {int b, double a =&gt;f(a,b);}

    a++

  10. spiceguid

    Est-ce que l’extension BGGA va jusqu’au point où cette fermeture:

    
    
    (&lt;A&gt;,&lt;B&gt; =&gt; &lt;C&gt;) =&gt; (&lt;B&gt;,&lt;A&gt; =&gt; &lt;C&gt;)&nbsp;<br />
    flip =&nbsp;<br />
    { (&lt;A&gt;,&lt;B&gt; =&gt; &lt;C&gt;) f =&gt; {&lt;B&gt; b, &lt;A&gt; a =&gt; f(a,b)} }

    renverrait une nouvelle fonction f’ similaire à f mais avec inversion de l’ordre d’application des 2 arguments ?

    L’inversion de cet ordre d’application étant très bien visible sur la signature (, => ) => (, => ).

    Cela reste tout de même assez verbeux comparé au code OCaml équivalent:

    let flip f x y = f y x

Laisser un commentaire