Tutoriel d'introduction à la programmation fonctionnelle avec Guava

Thierry Leriche-Dessirier

Guava est une bibliothèque, de chez Google, proposant de nombreux outils pour améliorer les codes des programmes Java. Elle permet, entre autres, de manipuler les collections, de jouer efficacement avec les immutables, d'éviter la gestion des beans nuls, de s'essayer à la programmation fonctionnelle, de cacher les objets, de les simplifier, et bien d'autres choses…

Dans ce troisième article sur Guava, nous allons découvrir en quoi la bibliothèque nous permet de faire de la programmation fonctionnelle. Commentez

- Titre : Tutoriel d'introduction à la programmation fonctionnelle avec Guava

- Auteur : Thierry Leriche-Dessirier

- Parution : 21 juillet 2013

- Licence : Les sources présentées sur cette page sont libres de droits et vous pouvez les utiliser à votre convenance. Par contre, la page de présentation constitue une œuvre intellectuelle protégée par les droits d'auteur. Copyright ® 2013 Thierry Leriche-Dessirier. Aucune reproduction, même partielle, ne peut être faite de ce site et de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.

Sommaire

- I. Introduction

- I-A. Versions des logiciels et bibliothèques utilisées

- I-B. Mises à jour

- II. Programmation fonctionnelle

- II-A. Predicates et Functions

- II-B. Filtres

- II-B-1. lazy

- II-B-2. Prédicats tout prêts

- II-B-3. Composition

- II-C. Transformations

- II-D. FluentIterables

- II-E. Et le reduce

- III. Conclusion

- IV. Remerciements

- V. Annexes

- V-A. Liens

- V-B. Liens personnels

- V-C. Le parseur de CSV avec Guava

I - Introduction

Cet article est le troisième d'une série consacrée à la bibliothèque Guava :

- introduction et installation ;

- collections ;

- programmation fonctionnelle ;

- utilitaires ;

- cache et concurrence ;

- tout pour vos Strings et primitifs ;

- un peu de math ;

- hash et I/O.

I-A - Versions des logiciels et bibliothèques utilisées

Pour écrire ce document, j'ai utilisé les versions suivantes :

- Java JDK 1.6.0_24-b07 ;

- Eclipse Indigo 3.7 JEE 64b ;

- Maven 3.0.3 ;

- JUnit 4.10 ;

- Guava 14.0.

J'utilise Java 6, car Java 7 n'est pas encore très répandu en entreprise. C'est ce que je vérifie durant mes conférences lorsque je demande qui utilise Java 7 sur ses serveurs de production, mais que très peu de mains se lèvent…

I-B - Mises à jour

1er septembre 2013 : création ;

6 octobre : Ajout du parseur CSV en annexes.

6 octobre : Ajout de la fonction reduce, proposée par Yann Caron.

II - Programmation fonctionnelle

Un des points qui reviennent souvent, lorsqu'on parle de Guava, c'est que la bibliothèque permet de s'adonner à la programmation fonctionnelle en Java sans avoir à attendre les Lambdas, prévus pour la version 8 du langage.

Je préfère le dire dès le départ, Guava ne s'inscrit pas en concurrence avec les Lambdas. L'équipe Guava a été très claire sur ce point. D'ailleurs, avec l'arrivée prochaine de Java 8, l'équipe a décidé de ne plus rien ajouter (hors débogage) sur le sujet de la programmation fonctionnelle.

Je voudrais insister également sur le fait qu'il ne faut chercher aucune optimisation dans la programmation fonctionnelle à travers Guava. Toutefois il est vrai que la bibliothèque va vous permettre de mieux organiser/découper votre code, ce qui va mécaniquement le rendre plus performant…

II-A - Predicates et Functions

En plus des « iterables », il n'y a que deux classes à connaître pour jouer avec la programmation fonctionnelle de Guava : le « Predicate » et la « Function. Ces deux objets définissent la méthode « apply(..) » qu'il faut redéfinir.

Dans un « Predicate », la méthode « apply(..) » renvoie un booléen :

Pradicate simple

Predicate<Chien> malePredicate = new Predicate<Chien>() {
@Override
public boolean apply(Chien chien) {
return chien.getSex() == SexeEnum.MALE;
}
};

Dans une « Function », le type de retour est paramétré :

Function<Chien, String> chienNameFunction = new Function<Chien, String>() {
@Override
public String apply(Chien from) {
return from.getName();
}
};

Je devine que vous avez déjà deviné que les « Predicates » servent à filtrer, tandis que les « Functions » servent à transformer (ie. « mapper »).

II-B - Filtres

La définition d'un filtre est assez simple, car il suffit d'appliquer un « Predicate » à une collection. Il faut juste faire attention au fait que ça renvoie des itérables :

Filtre simple

@Test
public void testFilterMale1() {
// Arrange
final List<Chien> chiens = Lists.newArrayList(
new Chien("Milou", MALE),
new Chien("Pluto", MALE),
new Chien("Lassie", FEMALE),
new Chien("Volt", MALE),
new Chien("Rantanplan", MALE),
new Chien("Idefix", MALE));
final int nb = 5;

// Act
final Predicate<Chien> malePredicate = new Predicate<Chien>() {
@Override
public boolean apply(Chien chien) {
return chien.getSex() == SexeEnum.MALE;
}
};
final List<Chien> res = Lists.newArrayList(Iterables.filter(chiens, malePredicate));

// Assert
Assert.assertEquals(nb, res.size());
}

Je crois que c'est assez simple pour se passer d'explication ; la méthode « filter() » de Guava applique le prédicat « malePredicate » sur la collection « chiens ».

Les plus curieux d'entre vous trouveront un petit détail qui a son importance dans la documentation. Il est possible d'utiliser la classe « Collections2 » à la place de « Iterables ». La classe « Collections2 » renvoie donc une vue « live », non thread safe, des éléments filtrés tandis que Iterable renvoie une « copie ». La doc le dit elle-même, si une vue live n'est pas nécessaire alors l'utilisation de « Iterables » permettra d'avoir un programme plus rapide (dans la plupart des cas).

Ça marche aussi avec des maps, sur lesquelles on peut aussi traiter les clés :

Filtre sur les clés

@Test
public void testFilterSurMap() {
// Arrange
final Map<String, Integer> ages = new ImmutableMap.Builder<String, Integer>()
.put("Jean", 32)
.put("Paul", 12)
.put("Lucie", 37)
.put("Marie", 17)
.put("Bernard", 17)
.put("Toto", 17)
.put("Loulou", 17)
.build();
final int nb = 3; // Jean, Paul et toto

// Act
final Predicate<String> sizePredicate = new Predicate<String>() {

@Override
public boolean apply(String from) {
return from.length() < 5;
}
};

final Map<String, Integer> filtered = Maps.filterKeys(ages, sizePredicate);

// Assert
Assert.assertEquals(nb, filtered.size());
}

II-B-1 - lazy

À ce stade, il faut absolument comprendre que le filtre vous renvoie une « vue » et non une « simple » liste. Une vue est « lazy ». Ça veut dire que le filtre sera effectué à chaque fois que vous utiliserez la vue et pas seulement la première fois. En fait, le filtre est effectué à la demande. Si on ne demande rien alors il n'est pas affecté. Si on le demande trois fois, le filtre passe donc trois fois. Il n'y a pas de mise en cache (comme on pourrait en avoir avec JPA). Ajoutons un « println » pour bien voir que l'affichage et le filtre se font en même temps :

Avec quelques println

final List<Chien> chiens = Lists.newArrayList(
new Chien("Milou", MALE),
new Chien("Pluto", MALE),
new Chien("Lassie", FEMALE),
new Chien("Volt", MALE),
new Chien("Rantanplan", MALE),
new Chien("Idefix", MALE));

// Act
final Predicate<Chien> malePredicate = new Predicate<Chien>() {
@Override
public boolean apply(Chien chien) {
System.out.println("apply");
return chien.getSex() == SexeEnum.MALE;
}
};
Iterable<Chien> iter = Iterables.filter(chiens, malePredicate);

System.out.println("Boucle 1");
for (Chien chien : iter) {
System.out.println("1 : " + chien);
}

System.out.println("Boucle 2");
for (Chien chien : iter) {
System.out.println("2 : " +chien);
}

Sortie console

Boucle 1
apply
1 : Dog{name=Milou}
apply
1 : Dog{name=Pluto}
apply
apply
1 : Dog{name=Volt}
apply
1 : Dog{name=Rantanplan}
apply
1 : Dog{name=Idefix}
Boucle 2
apply
2 : Dog{name=Milou}
apply
2 : Dog{name=Pluto}
apply
apply
2 : Dog{name=Volt}
apply
2 : Dog{name=Rantanplan}
apply
2 : Dog{name=Idefix}

Vous remarquez que les affichages des chiens sont mélangés aux affichages des filtres. Ici le double affichage de « apply » correspond à Lassie, qui est une fille et qui est donc filtrée.

Vous remarquez aussi que les filtres sont réalisés pour chaque boucle. La parade la plus simple, pour ne faire les filtres qu'une seule fois, consiste à mettre le résultat dans une liste, à l'aide de « newArrayList(..) » qui accepte les itérables :

Dans une liste

final List<Chien> list = Lists.newArrayList(Iterables.filter(chiens, malePredicate));

System.out.println("Boucle 1");
for (Chien chien : list) {
System.out.println("1 : " + chien);
}

System.out.println("Boucle 2");
for (Chien chien : list) {
System.out.println("2 : " + chien);
}

Sortie console

apply
apply
apply
apply
apply
apply
Boucle 1
1 : Dog{name=Milou}
1 : Dog{name=Pluto}
1 : Dog{name=Volt}
1 : Dog{name=Rantanplan}
1 : Dog{name=Idefix}
Boucle 2
2 : Dog{name=Milou}
2 : Dog{name=Pluto}
2 : Dog{name=Volt}
2 : Dog{name=Rantanplan}
2 : Dog{name=Idefix}

Cette fois, le filtre est donc réalisé une seule fois, même si on ne s'en sert pas. Il va donc falloir choisir si vous voulez une vue lazy ou une liste, selon que vous souhaitez l'utiliser une seule fois (voire aucune) ou à plusieurs reprises.

II-B-2 - Prédicats tout prêts

Guava propose aussi des prédicats tout prêts. Ce sont des prédicats qui semblent relativement simples, mais qui sont en réalité très difficiles à programmer. Commençons avec le prédicat « in(..) » qui indique si un élément est présent dans la collection :

Is in

@Test
public void testIn() {
// Arrange
final List<Chien> chiens = Lists.newArrayList(
new Chien("Milou", MALE),
new Chien("Pluto", MALE),
new Chien("Lassie", FEMALE),
new Chien("Volt", MALE),
new Chien("Rantanplan", MALE),
new Chien("Idefix", MALE));
final String nom = "Volt";

// Act
final boolean isIn = Predicates.in(chiens).apply(new Chien(nom));

// Assert
Assert.assertTrue(isIn);
}

Les prédicats tout prêts vous offrent seulement la mécanique technique. Il faut que votre objet soit correctement écrit, notamment qu'il possède les méthodes classiques « equals() » et « hashCode() ».

On peut en demander un peu plus, en composant plusieurs prédicats de ce type, et même sur des collections différentes :

And

@Test
public void testAnd() {
// Arrange
final List<Chien> list = Lists.newArrayList(
new Chien("Milou", MALE),
new Chien("Pluto", MALE),
new Chien("Lassie", FEMALE),
new Chien("Volt", MALE),
new Chien("Rantanplan", MALE),
new Chien("Idefix", MALE));
final Set<Chien> set = Sets.newHashSet(
new Chien("Lassie", FEMALE),
new Chien("Volt", MALE),
new Chien("Medor", MALE));

final String nom = "Volt";

// Act
final boolean isIn = and(in(list), in(set)).apply(new Chien(nom));

// Assert
Assert.assertTrue(isIn);
}

Vous devinez déjà que vous pouvez utiliser les prédicats « or » et « not » en complément. Je vous épargne les explications évidentes.

II-B-3 - Composition

Comme son nom l'indique, la méthode « compose() » renvoie la composition de deux fonctions. Comme le dit si bien la Javadoc, si on prend une fonction f(x) et une fonction g(x), alors la composition h(x) sera égale à g(f(x)). Bon, en le disant en français, ça veut juste dire que, pour chaque élément qu'on lui passe, ça applique une première fonction, puis ça applique la seconde fonction au résultat de la première : une composition quoi…

Composition

@Test
public void testCompo() {
// Arrange
final List<Chien> chiens = Lists.newArrayList(
new Chien("Milou", MALE),
new Chien("Pluto", MALE),
new Chien("Lassie", FEMALE),
new Chien("Volt", MALE),
new Chien("Rantanplan", MALE),
new Chien("Idefix", MALE));

final String enminuscule = "milou";

final Function<Chien, Chien> majusculiser = new Function<Chien, Chien>() {
@Override
public Chien apply(Chien from) {
return new Chien(from.getName().substring(0, 1).toUpperCase() + from.getName().substring(1));
}
};

// Act
final boolean isIn = compose(in(chiens), majusculiser).apply(new Chien(enminuscule));

// Assert
Assert.assertTrue(isIn);
}

Ça pique un peu les yeux ? Voici ce que ça fait : dans un premier temps, ça passe le chien « milou » (avec tout le nom en minuscules) à la fonction. Celle-ci met en majuscule la première lettre, ce qui donne « Milou ». Le prédicat vérifie ensuite si un chien avec le nom « Milou » est présent dans la liste, ce qui est le cas.

C'est un peu difficile à utiliser et je vous invite à voir comment vous pouvez faire mieux et plus simple à l'aide des « FluentIterables » expliqués dans le prochain chapitre.

II-C - Transformations

Si vous avez compris le chapitre précédent, dédié aux filtres, vous allez vite comprendre celui-ci, car c'est la même chose, un niveau au-dessus, et pour faire des transformations.

Plus concrètement, l'idée va être de transformer une liste d'objets en une vue d'un autre type :

Transformation d'un chien en string

@Test
public void testTransform() {
// Arrange
final List<Chien> chiens = Lists.newArrayList(
new Chien("Milou", MALE),
new Chien("Pluto", MALE),
new Chien("Lassie", FEMALE),
new Chien("Volt", MALE),
new Chien("Rantanplan", MALE),
new Chien("Idefix", MALE));

final List<String> expected = Lists.newArrayList("Milou", "Pluto", "Lassie", "Volt", "Rantanplan", "Idefix");

// Act
final List<String> noms = Lists.transform(chiens, new Function<Chien, String>() {
@Override
public String apply(Chien from) {
return from.getName();
}
});

// Assert
Assert.assertEquals(expected, noms);
}

De manière générale, j'utilise toujours l'utilitaire « Lists », car il renvoie directement une liste. On peut toutefois passer par « Iterables » :

Chien to Super Dog

@Test
public void testTransformToSuperDog() {
// Arrange
final Set<Chien> chiens = Sets.newHashSet(
new Chien("Milou", MALE),
new Chien("Pluto", MALE),
new Chien("Lassie", FEMALE),
new Chien("Volt", MALE),
new Chien("Rantanplan", MALE),
new Chien("Idefix", MALE));
final String expectedPower = "Code en Java";

// Act
final Function<Chien, SuperDog> chienToSuperDogFunc = new Function<Chien, SuperDog>() {
@Override
public SuperDog apply(Chien from) {
final SuperDog to = new SuperDog();

to.setName(from.getName());
final List<String> powers = Lists.newArrayList("Vole", "Code en Java", "Court vite");
to.setPowers(powers);
return to;
}
};
final Iterable<SuperDog> iter = Iterables.transform(chiens, chienToSuperDogFunc);

// Assert
for(SuperDog sd : iter) {
Assert.assertTrue(sd.getPowers().contains(expectedPower));
}
}

Bien entendu, tout comme pour les filtres, ça retourne une vue lazy. Là encore il faudra décider de la stratégie à adopter.

II-D - FluentIterables

La plupart du temps, vous pourrez vous contenter d'un filtre ou d'une transformation. Mais vous aurez parfois besoin d'utiliser les deux. Pensez au « filter-map-reduce »… Bien entendu, il est toujours possible de les enchaîner en deux temps :

Filter-map en deux temps

@Test
public void testFilterMap() {
// Arrange
final List<Chien> chiens = Lists.newArrayList(
new Chien("Milou", MALE),
new Chien("Pluto", MALE),
new Chien("Lassie", FEMALE),
new Chien("Volt", MALE),
new Chien("Rantanplan", MALE),
new Chien("Idefix", MALE));

final List<String> expected = Lists.newArrayList("Milou", "Pluto", "Volt", "Rantanplan", "Idefix");

// Act

final Predicate<Chien> malePredicate = new Predicate<Chien>() {
@Override
public boolean apply(Chien chien) {
System.out.println("apply predicate");
return chien.getSex() == SexeEnum.MALE;
}
};

final Function<Chien, String> toNameFunc = new Function<Chien, String>() {
@Override
public String apply(Chien from) {
System.out.println("apply function");
return from.getName();
}
};

final Iterable<Chien> filtered = Iterables.filter(chiens, malePredicate);
final Iterable<String> mapped = Iterables.transform(filtered, toNameFunc);

final List<String> noms = Lists.newArrayList(mapped);

// Assert
Assert.assertEquals(expected, noms);
}

Bon, ce n'est pas la fin du monde de le faire en deux temps, mais on voudrait vraiment pouvoir tout faire d'un coup. Dans ce cas, on va plutôt utiliser « FluentIterable » qui permet d'enchaîner les opérations :

En un coup

final List<String> noms = FluentIterable.from(chiens)
.filter(malePredicate)
.transform(toNameFunc)
.toList();

Ici, je demande à Guava d'enchaîner le filtre (toujours bon de le mettre en premier), puis la transformation puis de me donner ça sous la forme de liste (immutable) pour bien finir. Si vous avez déjà regardé à quoi vont ressembler les lambdas de Java 8, ça devrait vous parler.

Et encore une fois, n'oubliez pas que c'est « lazy »…

À lire, un billet de blog intitulé « FluentIterable sur mon chien Guava ».

II-E - Et le reduce

Il ne manque que le « reduce » du « filter-map-reduce » pour que le tableau soit complet. Guava ne propose pas spécifiquement cette fonction mais il est possible de s'en tirer. Voyons comment avoir les âges de nos chiens, avec une moyenne au milieu :

Reduce, proposé par Yann Caron

List<Dog> dogs = Lists.newArrayList(
new Dog("effy", Dog.Gender.MALE, 5),
new Dog("wolf", Dog.Gender.MALE, 7),
new Dog("lili", Dog.Gender.FEMALE, 7),
new Dog("poupette", Dog.Gender.FEMALE, 10),
new Dog("rouquette", Dog.Gender.FEMALE, 11),
new Dog("rouky", Dog.Gender.MALE, 8),
new Dog("athos", Dog.Gender.MALE, 3));

//final float average = 0;

Optional<Float> average = FluentIterable
.from(dogs)
.filter(new Predicate<Dog>() {
@Override
public boolean apply(Dog t) {
// filter keep only males
return t.getGender() == Dog.Gender.MALE;
}
})
.transform(new Function<Dog, Float>() {
int index = 0;
float previousAverage = 0;

@Override
public Float apply(Dog f) {
float age = f.getAge();

// calculate
float prevSum = previousAverage * index; // step 1
index++;
float newSum = prevSum + age; // step 2
float newAverage = newSum / index; // step 3

previousAverage = newAverage;

return newAverage;
}
})
.last();

float test = (5 + 7 + 8 + 3) / 4F;
System.out.println(test);
System.out.println("Average of male ages : " + average);

Vous trouvez que ça pique les yeux ? Alors autant programmer carrément le « reduce » :

Fonction reduce(), proposée par Yann Caron

public class Iterables2 {

private Iterables2() {}

public static <F extends Object> F reduce(Iterable<F> itrbl, Function<List<F>, ? extends F> fnctn) {
F prevResult = null;
F result = null;

for (F elem : itrbl) {
if (prevResult == null) {
result = elem;
} else {
result = fnctn.apply(Lists.<F>newArrayList(prevResult, elem));
}
prevResult = result;
}
return result;
}
}

Du coup, le code s'en trouve simplifié :

l'âge des chiens, proposé par Yann Caron

float average = Iterables2.reduce(
FluentIterable
.from(dogs)
.filter(new Predicate<Dog>() {
@Override
public boolean apply(Dog t) {
// filter keep only males
return t.getGender() == Dog.Gender.MALE;
}
})
.transform(new Function<Dog, Float>() {

int index = 0;
float previousAverage = 0;

@Override
public Float apply(Dog f) {
return Integer.valueOf(f.getAge()).floatValue();
}
}), new Function<List<Float>, Float>() {

int currentIndex = 1;

@Override
public Float apply(List<Float> f) {
// recursive serie

float prevResult = f.get(0);
float currentAge = f.get(1);

currentIndex++;

// calculate
float prevSum = prevResult * (currentIndex - 1); // step 1
float newSum = prevSum + currentAge; // step 2
float newAverage = newSum / currentIndex; // step 3

return newAverage;
}
});

III - Conclusion

Vous savez maintenant de quoi il retourne quand on parle de cette fabuleuse programmation fonctionnelle que vous pouvez faire à l'aide de Guava. C'est très puissant et très simple. N'hésitez pas à consulter les autres épisodes de cette série pour découvrir les fonctionnalités fantastiques de la bibliothèque.

Vos retours nous aident à améliorer nos publications. N'hésitez donc pas à commenter cet article sur le forum : Commentez

IV - Remerciements

D'abord j'adresse mes remerciements à l'équipe Guava, chez Google, pour avoir développé une bibliothèque aussi utile et pour la maintenir. Je n'oublie pas tous les contributeurs qui participent notamment sur le forum Guava.

Plus spécifiquement en ce qui concerne cet article, je tiens à remercier l'équipe de Developpez.com et plus particulièrement Bernard Le Roux, Ricky81, Mickael Baron, Yann Caron, Logan, et Claude Leloup.

V - Annexes

V-A - Liens

Guava : https://code.google.com/p/guava-libraries/

Article « Simplifier le code de vos beans Java à l'aide de Commons Lang, Guava et Lombok »:
https://thierry-leriche-dessirier.developpez.com/tutoriels/java/simplifier-code-guava-lombok/

Blog sur Guava : https://blog.developpez.com/guava/

Article « J2SE 1.5 Tiger » par Lionel Roux :
https://lroux.developpez.com/article/java/tiger/

Article « Présentation de Java SE 7 » par F. Martini (adiGuba) :
https://adiguba.developpez.com/tutoriels/java/7/

Article « Fonction Object Design pattern - Tutoriel sur les foncteurs » par Yann Caron :
https://caron-yann.developpez.com/tutoriels/java/fonction-object-design-pattern-attendant-closures-java-8/

V-B - Liens personnels

Retrouvez ma page et mes autres articles sur Developpez.com à l'adresse
https://thierry-leriche-dessirier.developpez.com/#page_articles

Suivez-moi sur Twitter : @thierryleriche (https://twitter.com/thierryleriche)

V-C - Le parseur de CSV avec Guava

Voici le code d'un parseur de fichier CSV, proposé par Yann Caron :

CSV

String csv = "" +
"element 1.1, element 1.2, element 1.3, element 1.4\n" +
"element 2.1, element 2.2\n" +
"element 3.1, element 3.2, element 3.3\n" +
"element 4.1, element 4.2, element 4.3, element 4.4, element 4.5\n" +
"";

Iterables.all(Splitter.on('\n')
.split(csv),
new Predicate<String>() {
// parse row

@Override
public boolean apply(String t) {
// parse item

Iterables.all(Splitter.on(',')
.trimResults()
.omitEmptyStrings()
.split(t),
new Predicate<String>() {

@Override
public boolean apply(String t) {
System.out.println("Parse : " + t);
return true;
}
});

return true;
}
}
);