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.