BT

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

Contribuez

Sujets

Sélectionner votre région

Accueil InfoQ Articles Curryfication Java : méthodes & fonctions

Curryfication Java : méthodes & fonctions

Définition

Wikipédia définit la curryfication comme :

« l'opération qui fait passer une fonction à plusieurs arguments à une fonction à un argument qui retourne une fonction prenant le reste des arguments ».

Ainsi, la curryfication est la transformation d’une fonction à plusieurs paramètres en une suite de fonctions, chacune d'entre elles ayant un seul paramètre d’entrée.

Exemple

Soit f la fonction somme, qui pour tout (x , y) elle retourne la somme x + y

f  : ( x,y ) -> x+ y

f peut être transformée en une fonction g avec un seul paramètre ayant comme définition : g : x -> ( y -> f(x,y) ), ou simplement g : x -> y -> f(x,y) sans les parenthèses vu que l’opérateur -> est souvent considéré comme associatif à droite.

L’opération inverse (dual dans la théorie des algèbres) s’appelle « Uncurrying » en anglais.

La curryfication permet l’application partielle des fonctions dans le sens où on peut directement obtenir la fonction h qui incrémente par 1 avec h : x -> g (1) (à ne pas confondre entre fonction partielle et curryfication).

Les langages fonctionnels ont été les premiers servis par la curryfication. Par contre, les langages objets ont proposé des solutions avec surcharge de méthodes ou des paramètres par défaut.

Java 7 et Curryfication des méthodes

Outre la panoplie des fonctionnalités apportées par java 7, ainsi que les routines permettant de surpasser la réflexivité et l'introspection sur les classes et méthodes, il est possible de chercher, invoquer ou de curryfier des méthodes et même des constructeurs.

L’exemple ci-dessous montre comment on curryfie une méthode statique « add » :

    public static int add(int i, int j) {
        return i + j;
    }
    MethodHandle adder = lookup().findStatic(MethodHandleCase.class, "add", methodType(int.class, int.class, int.class));
    MethodHandle incrementer = insertArguments(adder, 1, 1);

    System.out.println(adder.invoke(1, 5)); // prints 6
    System.out.println(incrementer.invoke(5)); // prints 6

incrementer n’est qu’une version curryfiée de la méthode adder.

Java 8 et Curryfication des fonctions

Java 8 vient d'être enrichi par les Functions et les BiFunctions. Ces dernières constituent la version à deux paramètres d’une fonction.

BiFunction

La curryfication pour le cas des BiFunctions permet de transformer une BiFunction<T, U, R> en une Function<T, Function<U, R>>.

La manière la plus simple pour l'implémenter revient à définir l’interface fonctionnelle Function2<T, U, R> où la méthode curried permet de faire cette transformation :

    @FunctionalInterface
    public interface Function2<T, U, R> extends Function<T, Function<U, R>> {

       static <T, U, R> Function2<T, U, R> curried(BiFunction<T, U, R> f) {
            return t -> u -> f.apply(t, u);
        }
    
    }

L’opération inverse - transformer Function<T, Function<U, R>> en BiFunction<T, U, R> - peut être implémentée simplement avec la méthode par défaut unCurried suivante :

    default BiFunction<T, U, R> unCurried() {
            return (t, u) -> apply(t).apply(u);
    }

Exemple

Pour évaluer la somme de deux entiers, on va définir la fonction add comme suit :

final BiFunction<Integer, Integer, Integer> uncSum = (x1, x2) -> x1 + x2;

Suite à la curryfication ( x1 -> x2 -> x1 + x2 ), on aura :

Function2<Integer, Integer, Integer> cSum = Function2.curried(uncSum);

Le retour arrière vers la forme de uncSum se fait à l’aide de l'opération inverse qui est cSum.unCurried().

Grâce à la fonction cSum, on pourra factoriser l’opération de sommation avec :

    Function<Integer, Integer> add2 = cSum.apply(2);

    add2.apply(2) -> 4 -> égale à uncSum(2,2) 
    add2.apply(4) -> 6 -> égale à uncSum(2,4) 

TriFunction

Java 8 n’embarque pas des fonctions avec plus que deux paramètres. Mais rien n’empêche de définir les nôtres.

Commençons par la définition de TriFunction : une fonction à trois paramètres Q, T et U avec R le type de retour :

    @FunctionalInterface
    public interface TriFunction<Q, T, U, R> {

        R apply(Q q, T t, U u);

        default <V> TriFunction<Q, T, U, V> withThen(Function<? super R, ? extends V> after) {
            Objects.requireNonNull(after);
            return (q, t, u) -> after.apply(apply(q, t, u));
        }
        
    }

La version curryfiée sera Function3<Q, T, U, R> qui acceptera Q comme paramètre et qui produira une Function2 en T, U et R (peut être transformée en BiFunction).

    @FunctionalInterface
    public interface Function3<Q, T, U, R> extends Function<Q, Function2<T, U, R>> {
        Function2<T, U, R> apply(Q q);

        default <V> Function3<Q, T, U, V> withThen(Function2<? super U, ? super R, ? extends V> after) {
            Objects.requireNonNull(after);
            return q -> t -> u -> after.apply(u).apply(apply(q).apply(t).apply(u));
        }

        default <V> Function3<Q, T, U, V> withThen(Function<? super R, ? extends V> after) {
            Objects.requireNonNull(after);
            return q -> t -> u -> after.apply(apply(q).apply(t).apply(u));
        }
        
    }

Pour passer de TriFunction à Function3, on pourra curryfier avec :

   default Function3<Q, T, U, R> curried() {
        return q -> t -> u -> apply(q, t, u);
    }

L’opération inverse est :

    default TriFunction<Q, T, U, R> unCurried() {
        return (q, t, u) -> apply(q).apply(t).apply(u);
    }

BiConsumer

La curryfication peut être généralisée avec les Consumers comme suit :

  • Consumer2, la version éclatée avec le passage vers BiConsumer via la fonction unCurried
  • BiConsumer, la version compacte avec le passage vers BiConsumer via la fonction curried
    @FunctionalInterface
    public interface Consumer2<T,U> {
        Consumer<U> accept(T t);

        default BiConsumer<T,U> unCurried(){
            return (t, u) -> accept(t).accept(u);
        }
    }
    @FunctionalInterface
    public interface BiConsumer<T,U> {
        void accept(T t,U u);

        default Consumer2<T,U> curried() {
            return t -> u -> accept(t,u);
        }
    }

Projections

La curryfication est en elle-même une fonction qui permet de transformer des fonctions (manipule les dimensions des fonctions) qu’on peut modéliser par :

  • Les BiFunctions
    Function<BiFunction<T,U,R>, Function<T,Function<U,R>>> curried = Function2::curried;
    Function<Function<T,Function<U,R>>, BiFunction<T,U,R>> unCurried = f -> (t, u) -> f.apply(t).apply(u);
  • Les TriFunctions
    Function<TriFunction<Q,T,U,R>, Function<Q,Function<T,Function<U,R>>>> curried = f -> q -> t -> u -> f.apply(q, t, u);
    Function<Function<Q,Function<T,Function<U,R>>>, TriFunction<Q,T,U,R>> unCurried = f ->(q, t, u) -> f.apply(q).apply(t).apply(u);
  • Les BiConsumers
    Function<BiConsumer<T,U>, Consumer2<T,U>> curried = f -> t -> u -> f.accept(t,u);
    Function<Consumer2<T,U>,BiConsumer<T,U>> unCurried = f -> (t, u) -> f.accept(t).accept(u) ;

Conclusion

La curryfication ajoute un sucre syntaxique pour la manipulation des fonctions et la factorisation des opérations. Elle constitue un des piliers des langages fonctionnels, comme Haskell où toutes les fonctions sont curryfiées ou Scala qui permet le passage direct entre les deux représentations.

À 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 multi-nationales. Certifié Java, Spring et MongoDB, Slim est passionné par Scala et JEE.

Vous pouvez en savoir plus sur ses récents travaux sur son blog et le suivre sur Twitter : @ouertani.

Evaluer cet article

Pertinence
Style

Contenu Éducatif

BT