Publié par

Il y a 8 années -

Temps de lecture 16 minutes

Comparaison d’API Java de programmation fonctionnelle

Alors que le Java Community Process (JCP) a annoncé l’apparition de la programmation fonctionnelle dans le langage Java, avec l’introduction des expressions lambda (JSR 335: Lambda Expressions for the JavaTM Programming Language), est-il possible avec la version actuelle de Java de pratiquer ce paradigme ? A l’heure où ces lignes sont écrites, le JCP est en plein brainstorming sur le sujet. Il existe différentes propositions concernant la syntaxe à adopter pour la JSR 335 : une proposition factice, un prototype pour l’OpenJDK serait en cours, la proposition BGGA (comprenez Bracha, Gafter, Gosling et von der Ahé), etc. Mais aucune de ces syntaxes n’ont été officialisées. Toutefois, un premier brouillon devrait apparaître d’ici septembre 2011 en vue d’une sortie officielle a priori avec Java 8 en 2012.

En attendant, il existe un certain nombre d’API permettant aux développeurs d’utiliser la programmation fonctionnelle avec Java sans forcément changer de langage.

Dans cet article, après avoir introduit la programmation fonctionnelle, nous allons nous intéresser à l’état actuel de ce style de programmation dans Java au travers des API suivantes :

  • Guava
  • Functional Java
  • FunckyJFunctional
  • LambdaJ

En partant d’un exemple commun, nous verrons les différences qui caractérisent ces API.

Programmation fonctionnelle

La programmation fonctionnelle est un paradigme de programmation tout comme le sont la programmation impérative, la programmation procédurale et la programmation orientée objet. Elle est axée autour de la notion de fonction, prise au sens mathématique du terme.

Une fonction décrit une relation entre deux ensembles de valeurs. Pour une valeur du premier ensemble, la fonction fait correspondre une seule et unique valeur du second ensemble (l’inverse n’est pas forcément vrai). En mathématique, nous parlons de surjection. On peut voir la fonction comme un mapping.

En programmation fonctionnelle, une fonction est donc une opération déclarative qui est :

  • indépendante (ne dépend d’aucun état d’exécution en dehors du sien),
  • sans état ou stateless en anglais (ne possède aucun état d’exécution interne qui est conservé entre les appels),
  • déterministe (renvoie toujours le même résultat pourvu qu’on lui ait fourni les mêmes arguments), on parle aussi de transparence référentielle.

On peut en plus de stocker des fonctions dans des variables, définir des fonctions qui prennent en entrée d’autres fonctions et/ou retournent une fonction. Il est souvent considéré que des langages respectant un tel paradigme doivent donner la possibilité de créer des fonctions anonymes.

L’intérêt de la programmation fonctionnelle est de permettre d’obtenir un code où les effets de bord sont a priori réduits au minimum. Les applications sont alors plus robustes (même dans un contexte concurrent) et plus faciles à maintenir. Il est aussi plus simple au niveau compilateur de mettre en place des optimisations, comme la mémoization, la recherche de sous-expressions communes ou la parallélisation du code.

D’un autre côté, la programmation fonctionnelle nécessite d’adopter un mode de pensée différent de la programmation orientée objet (sans forcément être incompatible avec l’OOP comme le montre Scala). En principe, il est bon de connaître un certain nombre de concepts, fonctions usuelles, patterns, etc., avant de prétendre coder pleinement en programmation fonctionnelle. Sans être insurmontables, ces aspects viennent petit à petit à force de pratique et de curiosité.

Exemple commun

Pour tester les différentes implémentations de programmation fonctionnelle en Java, nous allons partir d’un exemple d’objets et d’un ensemble de méthodes à implémenter. Le but étant de voir de quelle manière chaque API permet d’exprimer le corps de ces méthodes.

La solution impérative est laissée en exercice. Si vous souhaitez comparer la solution impérative avec les solutions fonctionnelles présentées dans cet article, nous vous conseillons d’arrêter votre lecture à la fin de cette section, de réaliser l’exercice avec la solution impérative et de reprendre la lecture à la section suivante.

Les objets Java que nous allons utiliser représentent des personnes, définies par leur nom, âge et sexe :

public class Person {
    private String name;
    private int age;
    private Gender gender;

    // constructor…
    // setters & getters…
    // hashCode & equals…
}

Ici, Gender représentant le sexe de la personne. Il est définit comme suit :

public enum Gender {
    MALE, FEMALE;
}

Voici maintenant les méthodes à implémenter :

public Iterable<Person> getAdultsFrom(Iterable<Person> persons)

public Iterable<Person> getWomenFrom(Iterable<Person> persons)

public Iterable<Person> getAdultMenFrom(Iterable<Person> persons)

public int getSumOfAgesOf(Iterable<Person> persons)

public Map<Gender, Integer> getGenderCountOf(Iterable<Person> persons)

getAdultsFrom() filtre une liste de personnes pour n’en garder que les personnes majeures. getWomenFrom() effectue aussi un filtrage sur une liste de personnes, mais cette fonction n’en conserve que les femmes. Les algorithmes de ces deux implémentations sont très proches puisqu’il s’agit de filtrer des valeurs. Ce qui change entre les deux, c’est le critère (ou prédicat) sur lequel on filtre une valeur. En programmation fonctionnelle, cette fonction est souvent appelée filter.

getAdultMenFrom() représente un cas particulier. Cette méthode possède des similarités avec getAdultsFrom() et getWomenFrom(), mais ici le but de l’exercice sera de réutiliser le prédicat des méthodes getAdultsFrom() et getWomenFrom() pour les composer ensemble et obtenir un nouveau prédicat.

getSumOfAgesOf() effectue une addition des âges de personnes dans une liste (en vue par exemple de calculer l’âge moyen d’un groupe). getGenderCountOf() retourne une table contenant le nombre de personnes pour chaque sexe à partir de la liste de personnes donnée en paramètre. Ici aussi, malgré les apparences, les algorithmes sont proches. En effet, il s’agit de partir d’une liste et de la réduire pour obtenir un nouvel élément qui se construit au fur et à mesure du parcours de la liste. Ce qui change c’est la façon avec laquelle nous allons réduire la liste et l’élément retourné. En programmation fonctionnelle, une telle fonction est appelée reduce ou fold.

Guava

Guava est une API proposée par Google. En plus de proposer une amélioration de certaines API de l’Apache Commons, Guava offre notamment un ensemble de classes et d’interfaces permettant à ses utilisateurs de créer et manipuler des fonctions. Guava définit deux interfaces pour définir des fonctions :

  • Function<F, T> représente une fonction qui prend en paramètre un élément de type F et retourne un élément de type T. Dans cette interface, il faut définir la méthode apply().
  • Predicate<T> représente un prédicat qui est un cas particulier de fonction qui retourne uniquement un booléen.

A ces deux interfaces, Guava définit tout un ensemble de méthodes utilitaires en vue de manipulation de fonctions et de prédicats, ou de manipulation de collections à partir de fonctions ou prédicats.

Filter

Pour les méthodes getAdultsFrom() et getWomenFrom(), nous allons d’abord définir les prédicats :

Predicate<Person> IS_ADULT = new Predicate<Person>() {
    @Override
    public boolean apply(Person person) { return person.getAge() >= 18; }
};

Predicate<Person> IS_WOMAN = new Predicate<Person>() {
    @Override
    public boolean apply(Person person) { return person.getGender() == Gender.FEMALE; }
};

On peut alors écrire grâce à la méthode filter() de la classe Iterables :

public static Iterable<Person> getAdultsFrom(Iterable<Person> persons) {
    return Iterables.filter(persons, IS_ADULT);
}

public static Iterable<Person> getWomenFrom(Iterable<Person> persons) {
    return Iterables.filter(persons, IS_WOMAN);
}

Il est simple avec Guava de composer les prédicats. Par exemple, avec un import statique des méthodes de la classe Predicates, on peut écrire :

public static Iterable<Person> getAldultMenFrom(Iterable<Person> persons) {
    return Iterables.filter(persons, and(IS_ADULT, not(IS_WOMAN)));
}

Ce qui permet de ne conserver que les personnes majeures et masculines.

Reduce

Malheureusement, Guava n’offre pas de méthodes déjà construites permettant de réduire une liste.

Il faut noter que Guava propose la méthode transform() de la classe Iterables qui applique une fonction à chaque élément d’une collection.

Functional Java

Functional Java est une API très aboutie dédiée entièrement à la programmation fonctionnelle dans Java. Cette API permet de définir des fonctions quelque soit le nombre de paramètres (de 0 à 8), de les manipuler, de manipuler des n-uplets (_tuple_ en anglais), de jouer avec des acteurs dans un environnement concurrent. Functional Java met à disposition, dans des classes utilitaires, un ensemble de fonctions usuelles. Et pour les adeptes de la programmation fonctionnelle, Functional Java propose une représentation des monoïdes.

La particularité de Functional Java est aussi de redéfinir complètement les collections utilisées par l’API. Functional Java n’utilise pas java.util.ArrayList ou java.util.HashSet, mais utilise fj.data.Array ou fj.data.Set. Le seul point commun entre ces classes réside dans le fait qu’elles implémentent l’interface java.util.Iterable. Des fonctions de conversion existent entre Java Collection et Functional Java.

Par contre, la documentation est plutôt anémique pour une API de cette envergure. De plus, Functional Java fait une utilisation intensive des Java Generics rendant le code très verbeux.

Filter

Functional Java ne propose pas de classe spécifique pour écrire des prédicats. Cependant, il suffit de représenter un prédicat par une fonction (interface F<A, B>) qui prend un objet et retourne un booléen. Dans ce cas, on écrit le prédicat des filtres de la manière suivante :

F<Person, Boolean> IS_ADULT = new F<Person, Boolean>() {
    @Override
    public Boolean f(Person person) { return person.getAge() >= 18; }
};

F<Person, Boolean> IS_WOMAN = new F<Person, Boolean>() {
    @Override
    public Boolean f(Person person) { return person.getGender() == Gender.FEMALE; }
};

Les méthodes getAdultsFrom() et getWomenFrom() s’écrivent :

public Iterable<Person> getAdultsFrom(Iterable<Person> persons) {
    return Array.iterableArray(persons).filter(IS_ADULT);
}

public Iterable<Person> getWomenFrom(Iterable<Person> persons) {
    return Array.iterableArray(persons).filter(IS_WOMAN);
}

La composition de prédicat se fait au sein d’un autre prédicat. Il n’y a pas de fonction de composition comme peut le proposer Guava :

F<Person, Boolean> IS_ADULT_MAN = new F<Person, Boolean>() {
    @Override public Boolean f(Person person) {
        return and.f(IS_ADULT.f(person)).f(not(IS_WOMAN).f(person));
    }
};

public Iterable<Person> getAdultMenFrom(Iterable<Person> persons) {
    return Array.iterableArray(persons).filter(IS_ADULT_MAN);
}

Reduce

Pour l’écriture des méthodes getSumOfAgesOf() et getGenderDistributions(), la méthode foldLeft() permet d’aggréger un ensemble de données. foldLeft() est définit au niveau des collections de Functional Java. Dans notre exemple, son utilisation passe par la définition de fonctions permettant d’aggréger partiellement des donnnées. La fonction SUM_OF_AGES prend en entrée un résultat partiel et une personne, et retourne le résultat partiel augmenté de l’âge de la personne. La fonction ADD_GENDER prend en entrée un tableau associant sexe et effectif ainsi qu’une fonction pour ajouter 1 dans la case du tableau représentant le sexe de la personne.

F2<Integer, Person, Integer> SUM_OF_AGES = new F2<Integer, Person, Integer>() {
    @Override public Integer f(Integer sumOfAges, Person person) {
        return sumOfAges + person.getAge();
    }
};

F2<Map<Gender, Integer>, Person, Map<Gender, Integer>> ADD_GENDER
  = new F2<Map<Gender, Integer>, Person, Map<Gender, Integer>>() {
    @Override
    public Map<Gender, Integer> f(Map<Gender, Integer> genderDistribution, Person person) {
        if (!genderDistribution.containsKey(person.getGender())) {
            genderDistribution.put(person.getGender(), 0);
        }
        // increase the gender distribution
        genderDistribution.put(person.getGender(), genderDistribution.get(person.getGender()) + 1);
        return genderDistribution;
    }
};

La fonction ADD_GENDER ne respecte pas tout à fait le contrat de la programmation fonctionnelle, car elle crée un effet de bord en modifiant l’un de ses paramètres. En fait, il aurait fallu créer à chaque appel de la fonction un nouveau tableau associatif.

Dans la définition ci-dessous des méthodes getSumOfAgesOf() et getGenderDistributionOf(), la méthode foldLeft() prend en paramètre une fonction et une valeur qui sert à l’initialisation.

public int getSumOfAgesOf(Iterable<Person> persons) {
    return Array.iterableArray(persons).foldLeft(SUM_OF_AGES, 0);
}

public Map<Gender, Integer> getGenderDistributionOf(Iterable<Person> persons) {
    return Array.iterableArray(persons).foldLeft(ADD_GENDER, new HashMap<Gender, Integer>());
}

FunckyJFunctional

FunckyJFunctional est une API développée initialement par Pierre-Yves Ricau. C’est une API qui vient en complément d’autres et propose une réduction de la verbosité. Il est possible de greffer FunckyJFunctional au-dessus de Guava, de FEST-Assert, de Java (pour les comparator, runnable et callable), etc. Il est aussi prévu dans des versions futurs de pouvoir greffer FunkyJFunctional avec FunctionalJava.

FunkyJFunctional exploite astucieusement le bloc d’initialisation, qu’on peut définir dans les classes, pour représenter le corps d’une fonction. Du coup, les fonctions sont instanciées en créant de nouvelles classes et non par des objets comme le font Guava et Functional Java. La variable de retour et les paramètres sont pré-déclarés, il ne reste plus qu’à les utiliser. Pour mettre en pratique ces classes-fonctions en utilisant Guava comme sous-jacent, il faut passer par des méthodes utilitaires de FunkyGuava comme withPred() qui crée un Predicate Guava ou withFunc() qui crée une Function Guava.

Filter

Les prédicats sont simple à définir :

class IsAdult extends Pred<Person> {{ out = in.getAge() >= 18; }}
class IsWoman extends Pred<Person> {{ out = in.getGender() == Gender.FEMALE; }}

En important statiquement les méthodes de la classe FunckyGuava et celle de certaines classes utilitaires de Guava, on peut écrire :

public Iterable<Person> getAdultsFrom(Iterable<Person> persons) {
    return Iterables.filter(persons, withPred(IsAdult.class));
}

public Iterable<Person> getWomenFrom(Iterable<Person> persons) {
    return Iterables.filter(persons, withPred(IsWoman.class));
}

public Iterable<Person> getAdultMenFrom(Iterable<Person> persons) {
    return Iterables.filter(persons,
                            and(withPred(IsAdult.class),
                                not(withPred(IsWoman.class))));
}

Reduce

Comme FunkyJFunctional dans sa version stable ne prend pas en compte Functional Java, on ne peut écrire les méthodes getSumOfAgesOf() et getGenderDistributionOf() sans sortir du cadre de l’API.

LambdaJ

LambdaJ est un framework qui utilise de manière intensive la réflexion pour pouvoir proposer une modification partielle du langage java et d’offrir une approche fonctionnelle.

Bien qu’elle permet de définir des fonctions qu’elle appelle Closure, la particularité de LambdaJ est aussi et surtout de fournir des outils puissants permettant d’améliorer l’expérience avec les collections et notamment de proposer un DSL interne qui rappelle fortement le SQL. En soit, LambdaJ ne s’arrête pas seulement au paradigme fonctionnel, mais se rapproche plus vers une approche déclarative où l’utilisateur décrit le problème et laisse l’ordinateur lui trouver une solution. Dans la liste des APIs vues dans cet article, LambdaJ est un outsider intéressant à étudier.

Filter

LambdaJ ne définit pas de prédicat à proprement parlé, LambdaJ définit des matchers, dérivant de Hamcrest qui est utilisé dans JUnit 4.x :

HasArgumentWithValue<Person, Integer> IS_ADULT
    = having(on(Person.class).getAge(), greaterThanOrEqualTo(18));

HasArgumentWithValue<Person, Gender> IS_WOMAN
    = having(on(Person.class).getGender(), equalTo(Gender.FEMALE));

Ces matchers s’utilisent dans des méthodes select() :

public Iterable<Person> getAdultsFrom(Iterable<Person> persons) {
    return select(persons, IS_ADULT);
}

public Iterable<Person> getWomenFrom(Iterable<Person> persons) {
    return select(persons, IS_WOMAN);
}

public Iterable<Person> getAdultMenFrom(Iterable<Person> persons) {
    return select(persons, IS_ADULT.and(not(IS_WOMAN)));
}

Reduce

La méthode getSumOfAgesOf() est vraiment simple car l’API définit déjà une fonction Sum. il en va de même pour getGenderDistributionOf().

public int getSumOfAgesOf(Iterable<Person> persons) {
    return sumFrom(persons).getAge();
}

public Map<Gender, Integer> getGenderDistributionOf(Iterable<Person> persons) {
    return count(persons, on(Person.class).getGender());
}

sumFrom() et count() sont des agrégateurs prédéfinis dans LambdaJ. Mais il est possible de créer d’autres agrégateurs (http://code.google.com/p/lambdaj/wiki/LambdajExtensibility).

Conclusion

Nous avons vu ici un ensemble d’API permettant d’utiliser la programmation fonctionnelle dans Java.

Guava propose une API rudimentaire dans ce sens, mais qui peut satisfaire bon nombre de développeurs Java. Dans Guava, on sera surtout intéressé par l’étendue des caractéristiques de l’API qui ne s’arrête pas seulement à la programmation fonctionnelle. Functional Java est extrêmement complet et se rapproche plus de Scala. L’API peut satisfaire les développeurs ayant une expérience plus avancée en programmation fonctionnelle. Cependant, avec une API aussi verbeuse du fait des generics, on peut se demander si changer de langage ne serait pas une nécessité (il est à noter que les tests de l’API Functional Java sont écrits en Scala).

FunkyJFunctional de son côté a trouvé une solution astucieuse à la verbosité contrainte par Java. L’API n’est pas aussi fournie que Functional Java, mais reste tout à fait exploitable pour bon nombre de développeurs. FunkyJFunctional a le mérite de simplifier l’expérience de la programmation fonctionnelle dans Java en proposant une API moins verbeuse. Toutefois, en dehors de sa simplicité, FunkyJFunctional dans son état actuel reste comparable à Guava dans le sens où l’API est une introduction à la programmation fonctionnelle. LambdaJ est une autre solution ingénieuse, faisant un usage intensif de la réflexion afin de réduire là aussi le verbosité de Java tout en augmentant la capacité d’expression de son utilisateur. Cependant, les auteurs reconnaissent que le fait de passer par la réflexion fait de LamdaJ une solution plus lente par rapport à une solution pure Java (en moyenne 3 fois plus lente).

Dans ce comparatif, on s’aperçoit que c’est en particulier la verbosité de Java qui représente un obstacle à la programmation fonctionnelle. Cet obstacle n’est pas insurmontable. Mais il est suffisamment imposant pour que les solutions existantes restent peu attractives au regard des alternatives, comme celle de changer de langage. L’introduction des expressions lambda dans Java (prévue a priori pour Java 8 en 2012) devrait apporter une solution définitive à ce problème. Mais permettra-t-elle de bénéficier des optimisations liées à la programmation fonctionnelle ?

[Edit (2011-06-30) : prise en compte du commentaire Nicolas François.]

Publié par

Publié par François Sarradin

Consultant Java et λ développeur. Blog personnel : http://kerflyn.wordpress.com/ Twitter : @fsarradin

Commentaire

9 réponses pour " Comparaison d’API Java de programmation fonctionnelle "

  1. Publié par , Il y a 8 années

    Article bien sympa.
    Par contre, un petit correctif à propos de FunckyJFunctional : le support de FunctionalJava, FestAssert et même Java existe depuis la version 1.0 (debut juin). Si je connais ce projet, c’est pour avoir contribué à la partie FestAssert.
    Ou alors y a t-il des fonctionnalités que tu aimerais voir et qui n’existe pas ? Tes idées sont les bienvenues.
    En tous cas, merci d’en parler :)

  2. Publié par , Il y a 8 années

    @Nicolas François: merci pour ces précisions. Je ferai les corrections nécessaires.

    Par contre, pour ce qui concerne Functional Java qui m’intéresse ici, je ne sais pas si j’ai bien lu, mais la Release Note indique que Functional Java est prévu pour la version 2.1 (https://github.com/pyricau/FunkyJFunctional/wiki/Release-Notes) qui n’est pas encore sortie. C’est ce qui semble se confirmer lorsqu’on parcourt le repo Github : en version 1.0 (considérée « stable »), il y a bien FEST-Assert, Guava, classic Java, Guava et Wicket, mais il n’y a pas Functional Java.

    Quoiqu’il en soit, FunkyJFunctional est un projet intéressant. Il mérite qu’on en parle ;)

  3. Publié par , Il y a 8 années

    Juste un détail que j’ai relevé (malheureusement dans les premières phrases). La définition donnée pour le type de fonction correspond, si mes souvenirs sont bons, à une injection (une valeur de l’ensemble de départ n’a au plus qu’une image) et non à une surjection (toute valeur de l’ensemble d’arrivée possède un antécédent).
    Mis à part ces détails mathématiques, l’idée de comparer ces différentes solutions est intéressante. Elle aurait peut être été mieux mise en valeur dans une série d’articles vu la densité du sujet.

  4. Publié par , Il y a 8 années

    @François Sarradin : Ah oui, oups, c’est à partir de la snapshot actuelle que le support Functionnal Java est disponible et non pas la 1.0, d’où ma confusion.
    Ton article m’a donné l’idée de jeter un oeil à LambdaJ, pour voir s’il serait possible et s’il y a un interêt de lui fait un module FJF. :)

  5. Publié par , Il y a 8 années

    @Jocelyn LECOMTE: les notions d’injection et de surjection sont les plus perturbantes que je connaisse. Je vais partir d’une autre référence : http://mathworld.wolfram.com/ (qui est l’une des références en mathématiques les plus solides à ma connaissance). Pour l’injection, on trouve la définition suivante :

    Soit f: A → B, pour x ∈ A et y ∈ A, on a : x ≠ y ⇒ f(x) ≠ f(y)

    L’injection est aussi appelée correspondance « one-to-one ».

    Si je prends la fonction valeur absolue abs: R → R+ et que je prends x=1 et y=-1, j’ai bien x ≠ y, mais je n’ai pas abs(x) ≠ abs(y). Donc abs n’est pas une injection. Pourtant abs est bien une fonction qui fait correspondre une valeur de l’ensemble de départ R à une et une seule valeur de l’ensemble d’arrivée R+. En fait, abs correspond plus à une surjection : pour tout y de R+, il existe un x de R tel que y = f(x).

  6. Publié par , Il y a 8 années

    Merci pour ce travail.
    L’article est de qualité et c’est appréciable lorsque l’on veut commencer à appréhender la programmation fonctionnelle et que l’on n’a pas le niveau.

  7. Publié par , Il y a 8 années

    @Johan Martinsson: Merci pour le lien, je ne connaissais pas.

    Après une première lecture, il semble que Totallylazy est le chaînon manquant entre Guava et Functional Java. On peut faire fold avec (aka reduce), en plus du filter. Et surtout, L’API est très fournit.

  8. Publié par , Il y a 7 années

    L’article est vraiment sympa et très pédagogique sur la programmation fonctionnelle.
    Par contre, une nuance dans la définition d’une « surjection ».

    Soit f: E → F
    x → y = f(x)

    f est une surjection si pour tout y de F, il y a au moins un x de E tel que y=f(x)

    Merci pour cet article sympa!

Laisser un commentaire

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

Nous recrutons

Être un Xebian, c'est faire partie d'un groupe de passionnés ; C'est l'opportunité de travailler et de partager avec des pairs parmi les plus talentueux.