1. Introduzione
La programmazione funzionale (PF) è un paradigma di programmazione dove il mattone fondamentale non è l'oggetto né la procedura/metodo, ma la funzione in senso matematico. Nella PF l'attenzione è posta sulla descrizione di "cosa calcolare", non su "come calcolarlo".
Avete già incontrato singole idee della PF quando avete lavorato con le espressioni lambda e LINQ. Ma qual è la differenza? In pratica: l'OOP descrive oggetti e le loro interazioni, la programmazione procedurale è un insieme di passi, mentre la PF è la composizione di funzioni, il passaggio del comportamento come valore, il rifiuto della modifica dello stato (immutability) e l'assenza di side effects.
Perché adottare un nuovo paradigma?
- Codice più pulito, prevedibile e testabile.
- Semplificazione del supporto al multithreading ("niente stato — nessun problema").
- Conciseness e espressività (meno codice = meno bug).
- Astrazioni di alto livello, facilmente riutilizzabili.
Analogia
Immagina che un ristorante riceva un ordine: "preparare un'omelette". Lo chef imperativo esegue una lista di istruzioni: prendere le uova, romperle, sbatterle, friggere. Lo chef funzionale dice: res = omelette(uova) — opera con funzioni, astraendo dallo stato interno della cucina (beh, quasi).
In C# possiamo usare entrambi gli approcci. Questo rende il linguaggio molto flessibile e potente — specialmente nei progetti reali.
Concetti chiave della PF
1. Funzioni di ordine superiore
Le funzioni possono essere passate come parametri, restituite da altre funzioni e memorizzate in variabili. Avete già fatto questo con le espressioni lambda e i delegate. Nella PF queste "funzioni su funzioni" sono la base di tutto.
2. Funzioni pure
Una funzione è "pura" se il suo risultato dipende solo dai parametri e non modifica nulla al di fuori di sé (no side effects). Due chiamate identiche con gli stessi argomenti danno lo stesso risultato.
3. Immutabilità (Immutability)
I dati non vengono mutati "in place": il nuovo stato è un nuovo oggetto. Questo semplifica molto il ragionamento sul programma e aiuta nel multithreading.
4. Assenza di side effects
Una funzione non scrive su file, non modifica variabili globali, non disegna sullo schermo — semplicemente restituisce un risultato. Nella vita reale i side effects sono inevitabili, ma si cerca di isolarli ai bordi del sistema.
5. Composizione di funzioni
Una funzione può essere costruita da altre, come mattoncini. Per esempio: filtrare i numeri positivi, prendere i loro quadrati e sommarli. Ogni operazione è una funzione separata e si combinano facilmente (Where → Select → Sum).
2. PF in C#: dalla teoria alla pratica
C# è un linguaggio multiparadigma: supporta bene OOP, l'approccio procedurale e uno stile funzionale potente (con lambda, delegate, extension methods e LINQ).
Vediamo un esempio nella nostra app didattica
Immaginiamo di sviluppare un programma per lavorare con liste di numeri e stringhe. Il nostro compito è applicare a questi dati varie operazioni in stile funzionale.
Esempio 1: Uso delle funzioni di ordine superiore
// Applica un'azione a tutti gli elementi della lista
public static void ForEach<T>(List<T> items, Action<T> action)
{
foreach (var item in items)
{
action(item);
}
}
Utilizzo:
var numbers = new List<int> { 1, 2, 3, 4, 5 };
ForEach(numbers, n => Console.WriteLine(n * n)); // Funzione-parametro
Visto? Una funzione può essere "messa in una variabile" o passata come normale valore — proprio come una mela in cucina!
Esempio 2: Funzione pura
Una funzione che non modifica lo stato del programma e dipende solo dall'input:
int MultiplyByTwo(int x)
{
return x * 2;
}
- Non dipende da nulla di esterno.
- Non modifica nulla esternamente.
- Per x = 5 restituirà sempre 10.
Confronta con una funzione che usa e modifica una variabile globale:
int total = 0;
int AddToTotal(int x)
{
total += x;
return total;
}
Questa non è più una funzione pura — il risultato dipende dallo stato esterno e la funzione lo modifica.
Esempio 3: Immutabilità dei dati
Invece di modificare i dati in ingresso creiamo nuovi oggetti:
List<int> AddOneToEach(List<int> numbers)
{
return numbers.Select(n => n + 1).ToList();
}
La lista di input non viene modificata. Nei programmi multithread questo è particolarmente comodo: meno lock e meno race.
Esempio 4: Composizione di funzioni
Ottenere la somma dei quadrati di tutti i numeri pari:
int SumOfEvenSquares(List<int> numbers)
{
return numbers
.Where(n => n % 2 == 0) // Tenere solo i pari
.Select(n => n * n) // Elevare al quadrato
.Sum(); // Sommare
}
Leggibile e dichiarativo: ogni operazione è una funzione separata.
3. Sfumature utili
PF, LINQ e C#
LINQ è quasi "PF in pratica" per le collezioni: usi funzioni di ordine superiore (Where, Select ecc.), ottieni nuove sequence senza mutare quelle originali, e ogni trasformazione è un'espressione a sé. Il risultato è IEnumerable<T>, che descrive cosa ottenere, non come iterare.
Tabella di analogie
| Imperativo (procedurale/OOP) | Funzionale (LINQ/stile PF) |
|---|---|
|
|
| «Mutare» la collezione | Ottenere una nuova collezione |
| Stato (total += x) | Funzioni pure (xs.Sum()) |
| Descrivere come: «fai questo» | Descrivere: «cosa vogliamo ottenere» |
PF vs OOP: due mondi — un solo C#
Non sono campi in competizione. Nei progetti C# reali li si combina: il modello del dominio (domain model) è comodo da costruire con classi (OOP), mentre il processamento delle collezioni, l'aggregazione dei dati e le trasformazioni si fanno in stile funzionale con LINQ, lambda ed extension methods.
La tua conoscenza dei delegate è direttamente utile: Func<T, TResult>, Predicate<T>, Action<T> — sono i mattoni tipici dello stile PF.
Funzione universale di filtraggio:
List<T> Filter<T>(List<T> items, Predicate<T> predicate)
{
var result = new List<T>();
foreach (var item in items)
{
if (predicate(item))
result.Add(item);
}
return result;
}
Chiamate:
var adults = Filter(people, person => person.Age >= 18);
var bigFiles = Filter(fileNames, name => name.EndsWith(".mp4") && name.Length > 10);
Invece di molte funzioni simili con condizioni diverse — una sola funzione universale.
Perché i datori di lavoro e i colloqui apprezzano sviluppatori PF?
- PF aiuta a testare piccoli blocchi di codice senza avviare tutto il sistema.
- La logica è più semplice da mantenere: meno stati = meno sorgenti di bug.
- È più semplice scrivere codice parallelo e asincrono — niente stato globale, meno race.
E come non diventare fanatici?
Sì, la PF è potente. Ma C# non è un linguaggio puramente funzionale, e non tutte le task richiedono purezza perfetta. Non temere variabili locali e mutazioni ragionevoli dove hanno senso. L'importante è leggibilità, prevedibilità e testabilità. Gli elementi PF sono uno strumento, non una religione.
4. Errori tipici dei principianti
È molto facile imbrogliarsi con codice che sembra funzionale ma in realtà non lo è.
Per esempio, una funzione restituisce una nuova collezione, ma lungo il percorso muta la lista originale — questo viola il principio di immutabilità e rompe le aspettative del chiamante.
Altro esempio: una lambda accede a una variabile esterna e la modifica. Nella paradigma funzionale questo è considerato un side effect e rende il comportamento del codice meno prevedibile.
Il compilatore C# non ti fermerà: il linguaggio permette entrambe le cose. Perciò, nelle pratiche PF è importante assicurarsi che una funzione "viva da sola", non modifichi nulla all'esterno e non legga altro se non i suoi argomenti.
GO TO FULL VERSION