BT

Diffuser les Connaissances et l'Innovation dans le Développement Logiciel d'Entreprise

Contribuez

Sujets

Sélectionner votre région

Accueil InfoQ Articles Pour le fun : for-comprehension à la sauce Java 8

Pour le fun : for-comprehension à la sauce Java 8

J’ai beaucoup hésité avant d'écrire cet article, étant donné qu'il s’agit à l’origine d’un défi personnel. Avec mes modestes connaissances en Scala, le but est d’imiter For-comprehension en utilisant Java. Nous allons essayer de lever une partie de ce défi sans donner de cours sur les monades et sans avoir recours aux macros.

For-comprehension avec Scala

En Scala, for-comprehension n’est rien de plus qu’un sucre syntaxique ; “for” n’est même pas un mot du langage. Sous le capot se cachent plusieurs transformations et chaînages monadiques. Heureusement, ils ne font pas l'objet de cet article.

Dans un premier temps, nous allons nous focaliser sur Option et Optional, respectivement pour Scala et Java. Comme hypothèse, soit les trois méthodes représentées dans le tableau suivant :

Java Scala Description
Optional m1() def m1 : Option[Int] m1 est sans paramètre
Optional m2(Integer i) def m2( i: Int):Option[String] m2 est supposé prendre le résultat de m1 s’il est défini
Optional m3(Integer i, String s) def m3( i:Int, s: String) : Option[String] m3 est supposé prendre les résultats de m1 et m2 s’ils sont définis

En Scala, il suffit d'écrire :

val value =    for {
      i1 <- m1
      i2 <- m2 (i1)
      i3 <- m3 (i1,i2)
    } yield i3

println(value)

L’enjeu est qu'on ne peut pas faire appel aux paramètres dans la signature avec Java. Exemple :

 void myFor( Integer i, “impossible de référencer i ici :(” )

For-comprehension avec Java

En 2010, j’ai publié un article qui essaie de traduire la fonctionnalité try-with-resource introduite avec java 7 en utilisation Scala. Nous allons faire l’inverse, étant donné que - à ma connaissance - seule la structure “try” de java 7 permet de faire référencer les paramètres au sein de la même signature.

Try-with-resources

Java 7 a introduit ce mécanisme de gestion des exceptions afin de rendre plus facile la manipulation des ressources, et qui permet de chaîner leurs déclarations. La contrainte est que les ressources doivent implémenter l’interface AutoCloseable, introduite également depuis java 7. Désormais, tout Closeable est un AutoCloseable avec une exception spécifique pour la méthode close. L’appel à la méthode close se fait dans l'ordre inverse des déclarations. On pourra écrire :

try(      FileInputStream     input         = new FileInputStream("file.txt");
          BufferedInputStream bufferedInput = new BufferedInputStream(input)
    ) {
        int data = bufferedInput.read();
        }
    }
}

For-with-resources

Les Optionals

Notre contrainte est de faire la liaison entre les Optionals et les AutoCloseables. Pour y arriver, définissons la classe générique Holder qui va servir en tant que conteneur et qui est à la fois AutoCloseable.

public class Holder<A> implements AutoCloseable {

    protected final A a;

    public Holder(A a) {
        this.a = a;
    }

    public A getValue() {
        return a;
    }

    @Override
    public String toString() {
        return a.toString();
    }

    @Override
    public void close() {
    }
}

Pour utiliser cette classe avec les Optionals, on définit la classe OptionalHolder comme suit :

public class OptionalHolder<A> extends Holder<Optional<A>> {

    public OptionalHolder(Optional<A> a) {
        super(a);
    }

    public A get() {
            return a.get();
    }
…
}

En premier lieu, on aura besoin d’une factory qui va nous permettre de créer des OptionalHolder à partir d’un Supplier d’Optional.

static <A> OptionalHolder<A> __(Supplier<Optional<A>> a) {
        return new OptionalHolder<>(a.get());
}

La méthode ‘__’ va nous permettre de faire compiler notre première instruction :

try (   OptionalHolder<Integer> i = __(Main::m1)){
              System.out.println(i);
  }

Avec le même nom de méthode ‘ __’, nous définissons une seconde instruction qui prend en paramètre le résultat de la première et qui va appliquer une transformation comme suit :

 static <A, B> OptionalHolder<B> __(OptionalHolder<A> a, Function<A, Optional<B>> f) {
        return new OptionalHolder(a.getValue().flatMap(f::apply ));
 }

Sous le capot se cache le chaînage des Optionals avec la méthode flatMap. Pour ceux qui sont encore allergiques aux lambdas, voici ci-dessous l'équivalant impératif :

if (a.getValue().isPresent()) {
            return new OptionalHolder(f.apply(a.get()));
        } else {
            return new OptionalHolder(Optional.empty());
}

Avec cette deuxième méthode, on pourra écrire :

try (   OptionalHolder<Integer> i1 = __(Main::m1);
         OptionalHolder<String> i2  = __(i1, i -> m2(i))
      ){
         System.out.println(i2);
 }

Si i1 est absent, il n’y aura plus appel à m2 et i2 sera un Optional vide. Sinon, i2 contiendra le résultat de m2. Ce qui est intéressant, c'est que si i1 n’est pas défini, nous n'avons pas de NullPointerExceptions.

Pour clôturer avec les Optionals, nous allons introduire la méthode suivante :

    
   static <A, B , C > OptionalHolder<C> __(OptionalHolder<A> a, OptionalHolder<B> b ,  BiFunction<A, B, Optional<C>> f) {
             return new OptionalHolder(a.getValue().flatMap(v -> b.getValue().flatMap((B t) -> f.apply(v, t))));
    }

Nous avons exercé un double chaînage, le calcul se termine une fois que les deux Optionals sont définis, sinon ce sera un Optional vide. L'équivalent impératif est :

static <A, B , C > OptionalHolder<C> f(OptionalHolder<A> a, OptionalHolder<B> b ,  BiFunction<A, B, Optional<C>> f) {
        if (a.getValue().isPresent() && b.getValue().isPresent()) {
            return new OptionalHolder(f.apply(a.get(),b.get()));
        } else {
            return new OptionalHolder(Optional.empty());
        }
}

Grâce à cette méthode, on pourra simuler For-comprehension avec Java :

  
  try (       OptionalHolder<Integer> i1 =  __(Main::m1);
                OptionalHolder<String>  i2  = __(i1, i -> m2(i));
                OptionalHolder<String> i3  = __(i1, i2, (i, s) -> m3(i, s))
        ){
              System.out.println(i3);
         }

Les Eithers

Les exemples ne se limitent pas aux Optionals. Même si Java 8 n’introduit pas les Eithers, on pourra utiliser la définition de Functionaljava. Généralement, on chaîne le calcul si Either est Right, alors que la valeur de Left est par défaut une exception.

De même qu'avec les Optionals, on va réutiliser notre classe Holder. Ci-dessous la version simplifiée pour les Eithers :

public class EitherHolder<L, R> extends Holder<Either<L, R>> {

    public EitherHolder(Either<L, R> a) {
        super(a);
    }

    static <L, R> EitherHolder<L, R> __(Supplier<Either<L, R>> a) {
        return new EitherHolder<>(a.get());
    }

    static <L, R1, R2> EitherHolder<L, R2> __(EitherHolder<L, R1> a, Function<R1, Either<L, R2>> f) {
        Either<L, R1> value = a.getValue();
        if (value.isRight()) {
            return new EitherHolder(f.apply(value.right().value()));
        } else {

            return new EitherHolder(value);
        }
    }

    static <L, R1, R2, R3> EitherHolder<L, R3> __(EitherHolder<L, R1> a, EitherHolder<L, R2> b, BiFunction<R1, R2, Either<L, R3>> f) {
        Either<L, R1> avalue = a.getValue();
        Either<L, R2> bvalue = b.getValue();
        if (avalue.isRight() && bvalue.isRight()) {
            return new EitherHolder(f.apply(avalue.right().value(), bvalue.right().value()));
        } else {
            if (avalue.isLeft()) {
                return new EitherHolder(avalue);
            } else {
                return new EitherHolder(bvalue);
            }
        }
    }
}

  

Ce qui permet d’écrire :

try (           EitherHolder<Exception ,Integer> i1 = __(Main::e1);
                EitherHolder<Exception , String> i2 = __(i1, i -> e2(i));
                EitherHolder<Exception , String> i3 = __(i1, i2, (i,s) -> e3(i,s))) {
            System.out.println( i3);
 }

Avec e1, e2 et e3 ayant les signatures respectives suivantes :

static Either<Exception,Integer> e1()
static Either<Exception,String> e2(Integer i)
static Either<Exception,String> e3(Integer i,String s)

Les Optionals et les Eithers

On pourra également penser à faire un mix de nos structures de données (monades). Chaîner par exemple d’un Either vers un Optional :

static <A,E, B> OptionalHolder<B> ___(EitherHolder<E,A> a, Function<A, Optional<B>> f) {
            if(a.getValue().isRight())
                return new OptionalHolder(f.apply(a.getValue().right().value()));

        return new OptionalHolder(Optional.empty());
 }

Et qui pourra être utilisé :

try ( EitherHolder<Exception ,Integer> i1 = __(Main::e1);
      OptionalHolder<String>           i2 =   ___(i1, i -> m2(i))) {
            System.out.println( i2);
}

Évidemment, on pourra faire d’autres combinaisons et avoir l'assurance de ne pas rencontrer des NullPointerExceptions.

Conclusion

Cet article est “just for fun” et à ne pas utiliser. L’imitation repose sur try-with-resource pour faire des for-comprehension. Ce qui est évident, c'est que l'écriture des DSL n’est pas aussi simple avec Java et que probablement, il y a d’autres façons d'arriver au même but sans faire ce qu’on appelle “try-comprehension”.

Références

À propos de l'Auteur

Slim Ouertani est un architecte logiciel avec une expérience dans le monde télécoms et systèmes d’information. Il a participé à la construction et la mise en place de plusieurs solutions notamment au sein de multinationales. Certifié Togaf, PMP, SOA Architect, SOA trainer, Java, Spring, ITIL, CMMi et MongoDB, Slim est passionné par Scala et JEE.

Vous pouvez en savoir plus sur ses récents travaux sur son [blog](http://ouertani.github.io/) et le suivre sur Twitter à [@ouertani](http://twitter.com/ouertani).

Evaluer cet article

Pertinence
Style

Contenu Éducatif

BT