Réduction des Streams

Le traitement des collections de données est l’une des opérations les plus fréquentes qu’on rencontre dans la majorité des applications de gestion.

Parmi ces traitements il y a l’opération de réduction qui consiste à parcourir tous les éléments d’une collection ou d’un stream afin de calculer une valeur résultante.

Dans cet article on explorera la réduction des streams en utilisant quelques concepts de la programmation fonctionnelle à travers des exemples se rapprochant des cas réels.

Le langage choisi est Java 8 qui dispose d’une API de Streams et des expressions lambda.

Prenons le cas d’une modélisation d’un achat dans un site d’e-commerce par exemple. Chaque achat est caractérisé par un ensemble de propriétés comme le numéro de l’achat, la date de l’achat ou le montant. Un utilisateur d’un tel site peut effectuer plusieurs achats à des dates différentes. 

Dans ce qui suit, nous calculerons de différentes manières le solde total des achats.

Un achat d’un produit est représenté de la manière suivante :

public class Achat {

  private double solde ;

  public Achat(double solde){
      this.solde = solde ;
  }

  public double getSolde() {
      return solde;
  }

  public void setSolde(double solde) {
      this.solde = solde;
  }
}

Méthode itérative de calcul de somme

On se propose de calculer la somme des achats fait sur le site.

Pour notre exemple utilisons un simple Stream de trois achats :

Achat achat1 = new Achat(10.0);
Achat achat2 = new Achat(5.5);
Achat achat3 = new Achat(4.5);
Stream<Achat> achats  = Stream.of(achat1, achat2, achat3);

Avec une méthode impérative, on peut calculer cette somme à travers une boucle, en parcourant le Stream d’achats tout en mutant une variable qui contiendra le résultat de tous les achats :

double sommeAchats = 0.0 ;
for(Achat achat : achats.collect(Collectors.toList())){
  sommeAchats += achat.getSolde() ;
}

Dans le code ci-dessus, on décrit comment on obtiendra le résultat. Pour ceci, on itère sur le tableau afin de rajouter à chaque itération la valeur de l’achat à la somme de l’achat. 

Streams Java 8

Une deuxième méthode, consiste à utiliser l’API Stream de Java 8 (qui est une des nouveautés majeures de cette version de Java). Cette API fournit un ensemble riche de fonctions pour les collections notamment map et sum.

Voyons comment utiliser ces deux fonctions pour calculer la somme des achats :

Double sommeAchats = achats.mapToDouble(Achat::getSolde).sum();

Avec map nous avons transformé le Stream d’objets de type Achat à un Stream de double.

Double streamDesSoldesAchats = achats.mapToDouble(Achat::getSolde)

Ensuite nous appelons simplement la méthode sum disponible dans le type  DoubleStream

Avec cette méthode, on obtient bien la somme des Achats, avec une méthode plus concise et expressive. Par contre, on a dû mapper le Stream d’achats en un Stream de double représentant le montant des achats pour calculer la somme. Est-il possible de se passer de cette étape ?

Les monoids

Commençons tout d’abord par définir une méthode dans la classe Achat qui réalise la somme de deux achats :

public static BinaryOperator<Achat> sommerAchats =  (achat1, achat2 ) -> new Achat(achat1.getSolde() + achat2.getSolde());

Il s’agit d’une méthode qui reçoit deux achats : achat1 et achat2 et qui produit un nouvel achat ayant un solde égal à la somme de ces deux achats.

Créons également une instance d’achat avec un solde à 0 comme suit :

public static final Achat ACHAT_ZERO = new Achat(0.0);

Avec cette méthode, on n’a plus besoin de mapper l’achat pour avoir son solde, on peut l’utiliser directement lors de la réduction avec la méthode reduce des Streams :

achats.reduce(Achat.ACHAT_ZERO , Achat.sommerAchats) // Achat(20.0)

En théorie, Achat  est considéré comme un Monoid ayant un élément neutre de type Achat,  Achat.ACHAT_ZERO et une fonction associative,  sommeAchatsFunction qui est une fonction qui prend deux achats et retourne comme résultat un achat.

« Soulever » des fonctions

On suppose qu’on dispose déjà d’une fonction curryfiée qui fait l’addition de deux doubles:

public static Function<Double, Function<Double, Double>> additionDouble = new Function<Double, Function<Double,Double>>() {
  @Override
  public Function<Double, Double> apply(Double x) {
      return ((Double y ) -> x + y);
  }
};

On souhaite utiliser cette fonction pour définir l’addition de deux achats.

Rappelons que l’addition de deux achats revient à l’addition de leurs deux soldes, donc il suffit d’appliquer cette fonction aux soldes des achats et par la suite créer un nouvel achat qui représente le résultat.

public static BinaryOperator<Achat> liftAchat(Function<Double, Function<Double, Double>> f){
  return ((Achat a, Achat b) -> new Achat(f.apply(a.getSolde()).apply( b.getSolde())));
}

La fonction lift permet d’appliquer la fonction additionDouble à des objets de type Achat sans avoir définir complètement cette fonction.

Au final on peut utiliser liftAchat pour obtenir la somme de deux achats en lui passant la fonction additionDouble :

public static BinaryOperator<Achat> sommeAchatsFunction5  = Achat.liftAchat(additionDouble) ;
...
Achat sommeAchats = achats.reduce(Achat.achatZero , Achat.sommeAchatsFunction5) ; // Achat(20.0)

Conclusion

Les Streams en Java 8 apportent de nombreuses nouvelles méthodes généralement rencontrées dans les langages fonctionnels. Elles permettent de réduire la surface de code avec une plus grande expressivité. N’hésitez pas à jouer avec l’API des Streams.

Laisser un commentaire

Votre adresse de messagerie ne sera pas publiée. Les champs obligatoires sont indiqués avec *