CodeGym /Cours /JAVA 25 SELF /Composition de fonctions: compose, andThen

Composition de fonctions: compose, andThen

JAVA 25 SELF
Niveau 49, Leçon 1
Disponible

1. Notion de composition de fonctions

Un peu de théorie (mais pas trop ennuyeuse)

En mathématiques, la composition de fonctions, c’est lorsque le résultat d’une fonction devient l’entrée d’une autre. Si nous avons des fonctions f et g, la composition g(f(x)) signifie: on applique d’abord f à x, puis on passe le résultat à g.

En programmation, l’idée est la même: nous voulons construire des transformations complexes à partir de transformations simples, au lieu d’écrire une énorme fonction qui fait tout. Cela rend le code flexible, réutilisable et lisible.

Imaginez une chaîne de pâtisserie: d’abord la pâte (f), puis la crème (g), puis le décor (h). L’ensemble du processus — c’est h(g(f(ingredients))).

Pourquoi la composition est-elle importante?

  • La composition permet d’assembler un programme à partir de petites fonctions « briques ».
  • Réutilisation facilitée: une « brique » peut être insérée à différents endroits sans duplication.
  • Flexibilité: pour modifier une étape, on remplace la fonction correspondante — le reste ne change pas.
  • Lisibilité et testabilité: de petites fonctions sont plus faciles à lire, vérifier et maintenir.

2. Méthodes compose et andThen dans l’interface Function

Interface Function: rappel

@FunctionalInterface
public interface Function<T, R> {
    R apply(T t);

    // Méthodes de composition:
    default <V> Function<V, R> compose(Function<? super V, ? extends T> before)
    default <V> Function<T, V> andThen(Function<? super R, ? extends V> after)
}
  • compose: la fonction passée à compose s’exécute d’abord, puis la fonction courante.
  • andThen: la fonction courante s’exécute d’abord, puis celle passée à andThen.

Schéma visuel

// Supposons qu’il y ait deux fonctions:
Function<String, Integer> parse = s -> Integer.parseInt(s);
Function<Integer, Integer> square = x -> x * x;

// compose: square.compose(parse) == x -> square.apply(parse.apply(x))
"5" --parse--> 5 --square--> 25

// andThen: parse.andThen(square) == x -> square.apply(parse.apply(x))
"5" --parse--> 5 --square--> 25

// Mais l’ordre est important si les types diffèrent!

Exemple: conversion d’une chaîne en nombre, puis en carré

import java.util.function.Function;

public class ComposeAndThenDemo {
    public static void main(String[] args) {
        // Fonction: convertit une chaîne en nombre
        Function<String, Integer> parse = s -> Integer.parseInt(s);

        // Fonction: élève un nombre au carré
        Function<Integer, Integer> square = x -> x * x;

        // Combinons: d’abord on parse, puis on met au carré
        Function<String, Integer> parseThenSquare = parse.andThen(square);

        System.out.println(parseThenSquare.apply("7")); // 49

        // Et si on inverse l’ordre?
        // square.compose(parse) — le même résultat (pour ces fonctions)
        Function<String, Integer> squareOfParsed = square.compose(parse);

        System.out.println(squareOfParsed.apply("8")); // 64
    }
}

Quand l’ordre a-t-il de l’importance?

Si les types des fonctions ne correspondent pas, l’ordre devient critique. Par exemple:

Function<String, String> addPrefix = s -> "User: " + s;
Function<String, Integer> length = s -> s.length();

Function<String, Integer> composed = addPrefix.andThen(length);
System.out.println(composed.apply("Alice")); // "User: Alice" -> 11

// Et comme ceci:
// length.andThen(addPrefix) — erreur de compilation!
// length renvoie Integer, tandis que addPrefix prend String.

Tableau: différence entre compose et andThen

f
g
f.compose(g)
f.andThen(g)
T → R
V → T
V → R: f(g(x))
T → V: g(f(x))

3. Composition de prédicats et d’autres interfaces

Predicate<T>: and, or, negate

L’interface fonctionnelle Predicate<T> est une fonction qui renvoie un boolean. Pour combiner des prédicats, il existe des méthodes dédiées:

  • and: ET logique (&&)
  • or: OU logique (||)
  • negate: NON logique (!)

Exemple: conditions de filtrage complexes

Supposons que nous ayons une classe utilisateur:

public class User {
    String name;
    int age;

    public User(String name, int age) {
        this.name = name;
        this.age = age;
    }
}

Écrivons maintenant différents prédicats:

import java.util.function.Predicate;

Predicate<User> isAdult = user -> user.age >= 18;
Predicate<User> nameStartsWithA = user -> user.name.startsWith("A");

// On combine: adulte et nom commençant par "A"
Predicate<User> adultAndA = isAdult.and(nameStartsWithA);

// Adulte ou nom commençant par "A"
Predicate<User> adultOrA = isAdult.or(nameStartsWithA);

// Non adulte
Predicate<User> notAdult = isAdult.negate();

On peut maintenant utiliser ces prédicats dans un filtrage, par exemple avec l’API Stream:

import java.util.List;
import java.util.stream.Collectors;

List<User> users = List.of(
    new User("Alice", 20),
    new User("Bob", 17),
    new User("Anna", 15),
    new User("Mike", 22)
);

List<User> filtered = users.stream()
    .filter(adultAndA)
    .collect(Collectors.toList());

// Dans filtered, seule Alice sera présente (adulte et nom commençant par "A")

Composition de Consumer, Function, Supplier

  • Consumer<T>: la méthode andThen permet d’exécuter deux opérations à la suite.
  • Function<T, R>: nous avons étudié la composition plus haut.
  • Supplier<T>: ne se combine pas directement, mais peut être utilisé à l’intérieur d’autres fonctions.

Exemple: Consumer<T>.andThen

import java.util.function.Consumer;

Consumer<String> print = s -> System.out.println("Reçu: " + s);
Consumer<String> printUpper = s -> System.out.println("En majuscules: " + s.toUpperCase());

Consumer<String> combined = print.andThen(printUpper);

combined.accept("hello");
// Affichera:
// Reçu: hello
// En majuscules: HELLO

4. Pratique: chaînes de transformations et filtrages

Exercice 1: constituer une chaîne de transformations Function

Supposons que, dans notre application, nous stockions les utilisateurs sous forme de chaînes "Nom,Âge", par exemple "Alice,20". Il faut:

  • Transformer la chaîne en objet User
  • Récupérer l’âge
  • Vérifier si l’utilisateur est majeur
import java.util.function.Function;
import java.util.function.Predicate;

Function<String, User> stringToUser = str -> {
    String[] parts = str.split(",");
    return new User(parts[0], Integer.parseInt(parts[1]));
};
Function<User, Integer> getAge = user -> user.age;
Predicate<Integer> isAdultAge = age -> age >= 18;

// Combinons: chaîne -> User -> âge -> prédicat
Function<String, Integer> stringToAge = stringToUser.andThen(getAge);

String input = "Bob,19";
int age = stringToAge.apply(input);
System.out.println("Âge: " + age); // 19

System.out.println("Majeur ? " + isAdultAge.test(age)); // true

Exercice 2: combiner plusieurs Predicate pour filtrer

Nous voulons sélectionner des utilisateurs de plus de 18 ans dont le nom commence par « A » ou « M ».

Predicate<User> isAdult = user -> user.age > 18;
Predicate<User> nameStartsWithA = user -> user.name.startsWith("A");
Predicate<User> nameStartsWithM = user -> user.name.startsWith("M");

Predicate<User> filter = isAdult.and(nameStartsWithA.or(nameStartsWithM));

List<User> filtered = users.stream()
    .filter(filter)
    .collect(Collectors.toList());

Exercice 3: transformation à plusieurs étapes avec Function

Objectif: obtenir une chaîne, supprimer les espaces superflus, convertir en majuscules, ajouter le préfixe "USER: ".

Function<String, String> trim = String::trim;
Function<String, String> toUpper = String::toUpperCase;
Function<String, String> addPrefix = s -> "USER: " + s;

// Construisons la chaîne
Function<String, String> pipeline = trim.andThen(toUpper).andThen(addPrefix);

System.out.println(pipeline.apply("   john   ")); // USER: VASYA

5. Erreurs typiques lors de la composition de fonctions

Erreur №1: ordre compose/andThen inversé.
Les débutants confondent souvent ce qui s’exécute en premier et en second. Retenez: f.compose(g) — d’abord g, puis f; f.andThen(g) — d’abord f, puis g.

Erreur №2: incompatibilité de types.
Si le type du résultat d’une fonction ne correspond pas au type du paramètre de l’autre, le compilateur refusera de les combiner. Par exemple, impossible de faire Function<Integer, String>.andThen(Function<Double, Boolean>).

Erreur №3: chaînes trop complexes.
Parfois on veut faire tenir toute la logique métier dans une seule chaîne et on obtient du « spaghetti ». Scindez en petites fonctions et donnez-leur des noms explicites.

Erreur №4: effets de bord dans les fonctions.
Il vaut mieux garder les fonctions et prédicats « purs » (sans effets de bord), sinon la composition devient risquée et imprévisible.

1
Mission
JAVA 25 SELF, niveau 49, leçon 1
Bloqué
Standardisation des identifiants pour la Société Secrète 🕵️‍♂️
Standardisation des identifiants pour la Société Secrète 🕵️‍♂️
1
Mission
JAVA 25 SELF, niveau 49, leçon 1
Bloqué
Transformations magiques des nombres à l'École des Sorciers 🧙‍♀️
Transformations magiques des nombres à l'École des Sorciers 🧙‍♀️
Commentaires
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION