CodeGym /Corsi /JAVA 25 SELF /Composizione di funzioni: compose, andThen

Composizione di funzioni: compose, andThen

JAVA 25 SELF
Livello 49 , Lezione 1
Disponibile

1. Il concetto di composizione di funzioni

Un po’ di teoria (ma senza essere pedanti)

In matematica, la composizione di funzioni è quando il risultato di una funzione diventa l’input di un’altra. Se abbiamo le funzioni f e g, allora la composizione g(f(x)) significa: prima applichiamo f a x, poi il risultato lo passiamo a g.

In programmazione l’idea è la stessa: vogliamo costruire trasformazioni complesse a partire da semplici, per non scrivere un’unica funzione gigantesca per ogni cosa. Questo rende il codice flessibile, riutilizzabile e leggibile.

Immagina una linea di produzione di pasticcini: prima l’impasto (f), poi la crema (g), poi la granella (h). L’intero processo è h(g(f(ingredienti))).

Perché la composizione è importante?

  • La composizione permette di costruire un programma da piccole funzioni-«mattoncini».
  • Più semplice da riutilizzare: un «mattoncino» può essere inserito in punti diversi senza duplicazione.
  • Flessibilità: per modificare una fase, sostituiamo la funzione corrispondente — il resto non si tocca.
  • Leggibilità e testabilità: le funzioni piccole sono più facili da leggere, verificare e manutenere.

2. Metodi compose e andThen nell’interfaccia Function

Interfaccia Function: promemoria

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

    // Metodi per la composizione:
    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: prima viene eseguita la funzione che passi a compose, poi — quella corrente.
  • andThen: prima viene eseguita la funzione corrente, poi — quella passata a andThen.

Schema visivo

// Supponiamo di avere due funzioni:
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

// Ma l'ordine è importante se i tipi sono diversi!

Esempio: convertire una stringa in numero e poi al quadrato

import java.util.function.Function;

public class ComposeAndThenDemo {
    public static void main(String[] args) {
        // Funzione: converte una stringa in un numero
        Function<String, Integer> parse = s -> Integer.parseInt(s);

        // Funzione: eleva un numero al quadrato
        Function<Integer, Integer> square = x -> x * x;

        // Combiniamo: prima facciamo il parse, poi eleviamo al quadrato
        Function<String, Integer> parseThenSquare = parse.andThen(square);

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

        // E se invertiamo l'ordine?
        // square.compose(parse) — lo stesso risultato (per queste funzioni)
        Function<String, Integer> squareOfParsed = square.compose(parse);

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

Quando l’ordine conta?

Se i tipi delle funzioni non coincidono, l’ordine diventa critico. Per esempio:

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

// E così:
// length.andThen(addPrefix) — errore di compilazione!
// length restituisce Integer, mentre addPrefix accetta String.

Tabella: differenza tra compose e andThen

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

3. Composizione di predicati e altre interfacce

Predicate<T>: and, or, negate

L’interfaccia funzionale Predicate<T> è una funzione che restituisce boolean. Per combinare i predicati ci sono metodi specifici:

  • and: AND logico (&&)
  • or: OR logico (||)
  • negate: NOT logico (!)

Esempio: condizioni di filtraggio complesse

Supponiamo di avere una classe utente:

public class User {
    String name;
    int age;

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

Ora definiamo predicati diversi:

import java.util.function.Predicate;

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

// Combiniamo: maggiorenne e nome che inizia per "A"
Predicate<User> adultAndA = isAdult.and(nameStartsWithA);

// Maggiorenne oppure nome che inizia per "A"
Predicate<User> adultOrA = isAdult.or(nameStartsWithA);

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

Ora possiamo usare questi predicati nel filtraggio, ad esempio tramite lo Stream API:

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());

// In filtered finirà solo Alice (maggiorenne e con nome che inizia per "A")

Composizione di Consumer, Function, Supplier

  • Consumer<T>: il metodo andThen — permette di eseguire due operazioni di seguito.
  • Function<T, R>: la composizione l’abbiamo vista sopra.
  • Supplier<T>: non si combina direttamente, ma può essere usato dentro altre funzioni.

Esempio: Consumer<T>.andThen

import java.util.function.Consumer;

Consumer<String> print = s -> System.out.println("Ricevuto: " + s);
Consumer<String> printUpper = s -> System.out.println("In maiuscolo: " + s.toUpperCase());

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

combined.accept("hello");
// Stamperà:
// Ricevuto: hello
// In maiuscolo: HELLO

4. Pratica: catene di trasformazioni e filtri

Esercizio 1: costruire una catena di trasformazioni Function

Supponiamo che nella nostra applicazione memorizziamo gli utenti come stringhe "Nome,Età", ad esempio "Alice,20". Occorre:

  • Convertire la stringa in un oggetto User
  • Ottenere l’età
  • Verificare se l’utente è maggiorenne
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;

// Combiniamo: stringa -> User -> età -> predicato
Function<String, Integer> stringToAge = stringToUser.andThen(getAge);

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

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

Esercizio 2: combinare più Predicate per il filtraggio

Supponiamo di dover scegliere gli utenti con più di 18 anni e il cui nome inizia per "A" o "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());

Esercizio 3: trasformazione a più fasi Function

Supponiamo sia necessario: prendere una stringa, rimuovere gli spazi ai bordi, convertirla in maiuscolo, aggiungere il prefisso "USER: ".

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

// Componiamo la pipeline
Function<String, String> pipeline = trim.andThen(toUpper).andThen(addPrefix);

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

5. Errori tipici nella composizione di funzioni

Errore n. 1: ordine compose/andThen invertito.
I principianti spesso confondono cosa viene eseguito per primo e cosa per secondo. Ricorda: f.compose(g) — prima g, poi f; f.andThen(g) — prima f, poi g.

Errore n. 2: disallineamento dei tipi.
Se il tipo del risultato di una funzione non coincide con il tipo del parametro dell’altra — il compilatore non permetterà di combinarle. Ad esempio, non si può fare Function<Integer, String>.andThen(Function<Double, Boolean>).

Errore n. 3: catene troppo complesse.
A volte si vuole comprimere tutta la logica di business in un’unica catena e ottenere «spaghetti code». Suddividi in piccole funzioni e dai loro nomi chiari.

Errore n. 4: effetti collaterali nelle funzioni.
È meglio mantenere funzioni e predicati «puri» (senza effetti collaterali), altrimenti la composizione diventa rischiosa e imprevedibile.

Commenti
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION